Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:memex.team:archdevops:system
pipewire-media-session
media-session-0.4.1.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File media-session-0.4.1.obscpio of Package pipewire-media-session
07070100000000000081A40000000000000000000000016178A88C00000068000000000000000000000000000000000000002600000000media-session-0.4.1/.codespell-ignoreba capela cas crasher datas endcode files' goin hda hist hve inport nd mmaped od ot parm sinc stdio uint07070100000001000081A40000000000000000000000016178A88C00000198000000000000000000000000000000000000002200000000media-session-0.4.1/.editorconfig# http://editorconfig.org root = true [*] indent_style = tab indent_size = 8 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true # Use 2 spaces for meson files [*.build] indent_style = space indent_size = 2 [*.yml] indent_style = space indent_size = 2 [*.{conf,conf.in}] indent_style = space indent_size = 4 [*.{xml,xml.in}] indent_style = space indent_size = 2 07070100000002000081A40000000000000000000000016178A88C00000201000000000000000000000000000000000000001F00000000media-session-0.4.1/.gitignore.tarball-version .version .*.swp ABOUT-NLS *~ *.tar.gz *.tar.xz *.o build/ builddir/ config.h.meson cscope.out cscope.in.out cscope.po.out Makefile subprojects/lua* subprojects/wireplumber subprojects/packagecache # Created by https://www.gitignore.io/api/vim ### Vim ### # Swap [._]*.s[a-v][a-z] [._]*.sw[a-p] [._]s[a-rt-v][a-z] [._]ss[a-gi-z] [._]sw[a-p] # Session Session.vim # Temporary .netrwhist *~ # Auto-generated tag files tags # Persistent undo [._]*.un~ # End of https://www.gitignore.io/api/vim 07070100000003000041ED0000000000000000000000036178A88C00000000000000000000000000000000000000000000001C00000000media-session-0.4.1/.gitlab07070100000004000081A40000000000000000000000016178A88C00001ED4000000000000000000000000000000000000002300000000media-session-0.4.1/.gitlab-ci.ymlstages: - container - container_coverity - build - analysis - pages variables: FDO_UPSTREAM_REPO: 'pipewire/media-session' # ci-templates as of March 19th 2021 .templates_sha: &templates_sha 290b79e0e78eab67a83766f4e9691be554fc4afd include: - project: 'freedesktop/ci-templates' ref: *templates_sha file: '/templates/fedora.yml' - project: 'freedesktop/ci-templates' ref: *templates_sha file: '/templates/ubuntu.yml' .fedora: variables: # Update this tag when you want to trigger a rebuild FDO_DISTRIBUTION_TAG: '2021-10-21.1' FDO_DISTRIBUTION_VERSION: '34' FDO_DISTRIBUTION_PACKAGES: >- alsa-lib-devel clang dbus-devel doxygen findutils gcc gcc-c++ git graphviz systemd-devel ShellCheck which valgrind ninja-build pkgconf python3-pip FDO_DISTRIBUTION_EXEC: >- pip3 install meson .ubuntu: variables: # Update this tag when you want to trigger a rebuild FDO_DISTRIBUTION_TAG: '2021-10-14.1' FDO_DISTRIBUTION_VERSION: '21.04' FDO_DISTRIBUTION_PACKAGES: >- libasound2-dev debhelper-compat findutils git libdbus-1-dev ninja-build pkg-config systemd python3-pip FDO_DISTRIBUTION_EXEC: >- pip3 install meson .coverity: variables: FDO_REPO_SUFFIX: 'coverity' FDO_BASE_IMAGE: registry.freedesktop.org/$FDO_UPSTREAM_REPO/fedora/$FDO_DISTRIBUTION_VERSION:$FDO_DISTRIBUTION_TAG FDO_DISTRIBUTION_PACKAGES: >- curl FDO_DISTRIBUTION_EXEC: >- mkdir -p /opt ; cd /opt ; curl -o /tmp/cov-analysis-linux64.tgz https://scan.coverity.com/download/linux64 --form project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN ; tar xf /tmp/cov-analysis-linux64.tgz ; mv cov-analysis-linux64-* coverity ; rm /tmp/cov-analysis-linux64.tgz only: variables: - $COVERITY .not_coverity: except: variables: - $COVERITY .build: before_script: # setup the environment - export BUILD_ID="$CI_JOB_ID" - export PREFIX="$PWD/prefix-$BUILD_ID" - export BUILD_DIR="$PWD/build-$BUILD_ID" - export XDG_RUNTIME_DIR="$(mktemp -p $PWD -d xdg-runtime-XXXXXX)" # Build pipewire - export PW_BUILD_DIR="$PWD/build-pipewire-$BUILD_ID" - git clone --depth=1 --branch="master" https://gitlab.freedesktop.org/pipewire/pipewire.git - meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX" -Dpipewire-alsa=disabled -Dpipewire-jack=disabled -Dalsa=disabled -Dv4l2=disabled -Djack=disabled -Dbluez5=disabled -Dvulkan=disabled -Dgstreamer=disabled -Dsystemd=disabled -Ddocs=disabled -Dman=disabled -Dexamples=disabled -Dpw-cat=disabled -Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled -Davahi=disabled -Decho-cancel-webrtc=disabled -Dsession-managers=[] -Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled - ninja -C "$PW_BUILD_DIR" install - export PKG_CONFIG_PATH="$(dirname $(find "$PREFIX" -name 'libpipewire-*.pc')):$PKG_CONFIG_PATH" script: - echo "Building with $MESON_OPTIONS" - meson "$BUILD_DIR" . --prefix="$PREFIX" $MESON_OPTIONS - ninja -C "$BUILD_DIR" - ninja -C "$BUILD_DIR" test - ninja -C "$BUILD_DIR" install artifacts: name: media-session-$CI_COMMIT_SHA when: always paths: - build-*/meson-logs container_ubuntu: extends: - .ubuntu - .fdo.container-build@ubuntu stage: container variables: GIT_STRATEGY: none # no need to pull the whole tree for rebuilding the image container_fedora: extends: - .fedora - .fdo.container-build@fedora stage: container variables: GIT_STRATEGY: none # no need to pull the whole tree for rebuilding the image container_coverity: extends: - .fedora - .coverity - .fdo.container-build@fedora stage: container_coverity variables: GIT_STRATEGY: none build_on_ubuntu: extends: - .ubuntu - .not_coverity - .fdo.distribution-image@ubuntu - .build stage: build .build_on_fedora: extends: - .fedora - .not_coverity - .fdo.distribution-image@fedora - .build stage: build build_on_fedora: extends: - .build_on_fedora artifacts: name: media-session-$CI_COMMIT_SHA when: always paths: - build-*/meson-logs - prefix-* parallel: matrix: - CC: [gcc, clang] # A release build with NDEBUG, all options on auto() but tests explicitly # enabled. This should show issues with tests failing due to different # optimization or relying on assert. build_release: extends: - .build_on_fedora variables: MESON_OPTIONS: "-Dtest=enabled -Dbuildtype=release -Db_ndebug=true" parallel: matrix: - CC: [gcc, clang] valgrind: extends: - .build_on_fedora script: - echo "Building with $MESON_OPTIONS" - meson "$BUILD_DIR" . --prefix="$PREFIX" $MESON_OPTIONS - meson test -C "$BUILD_DIR" --setup=valgrind build_with_coverity: extends: - .fedora - .coverity - .fdo.suffixed-image@fedora - .build stage: analysis script: - export PATH=/opt/coverity/bin:$PATH - meson "$BUILD_DIR" . --prefix="$PREFIX" - cov-configure --config coverity_conf.xml --comptype gcc --compiler cc --template --xml-option=append_arg@C:--ppp_translator --xml-option=append_arg@C:"replace/_sd_deprecated_\s+=/ =" --xml-option=append_arg@C:--ppp_translator --xml-option=append_arg@C:"replace/GLIB_(DEPRECATED|AVAILABLE)_ENUMERATOR_IN_\d_\d\d(_FOR\(\w+\)|)\s+=/ =" --xml-option=append_arg@C:--ppp_translator --xml-option=append_arg@C:"replace/(__has_builtin|_GLIBCXX_HAS_BUILTIN)\(\w+\)/1" - cov-build --dir cov-int --config coverity_conf.xml ninja -C "$BUILD_DIR" - tar czf cov-int.tar.gz cov-int - curl https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN --form email=$GITLAB_USER_EMAIL --form file=@cov-int.tar.gz --form version="`git describe --tags`" --form description="`git describe --tags` / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID " artifacts: name: media-session-coverity-$CI_COMMIT_SHA when: always paths: - build-*/meson-logs - cov-int/build-log.txt reserve.c sync: extends: - .build_on_fedora stage: analysis script: - >- curl https://gitlab.freedesktop.org/pipewire/pipewire/-/raw/master/src/tools/reserve.c > src/reserve.c - >- curl https://gitlab.freedesktop.org/pipewire/pipewire/-/raw/master/src/tools/reserve.h > src/reserve.h - git diff --exit-code || (echo "WARNING - reserve.{c|h} should be kept in sync with PipeWire" && exit 1) allow_failure: true shellcheck: extends: - .build_on_fedora stage: analysis script: - shellcheck $(git grep -l "#\!/.*bin/.*sh") spellcheck: extends: - .build_on_fedora stage: analysis script: - git ls-files | grep -v .gitlab-ci.yml | xargs -d '\n' sed -i 's/Pipewire/PipeWire/g' - git diff --exit-code || (echo "Please fix the above spelling mistakes" && exit 1) - git ls-files | grep -v .gitlab-ci.yml | xargs -d '\n' sed -i 's/Media \?session/Media Session/g' - git diff --exit-code || (echo "Please fix the above spelling mistakes" && exit 1) doccheck: extends: - .build_on_fedora stage: analysis script: # Check that each media session module has a \subpage entry - git grep -h -o -e "\\\page page_media_session_module_\w\+" | cut -f2 -d' ' > media_session_pages - cat media_session_pages - | for page in $(cat media_session_pages); do git grep -q -e "\\\subpage $page" || (echo "\\page $page is missing \\subpage entry in doc/media-session.dox" && false) done 07070100000005000041ED0000000000000000000000026178A88C00000000000000000000000000000000000000000000002C00000000media-session-0.4.1/.gitlab/issue_templates07070100000006000081A40000000000000000000000016178A88C00000000000000000000000000000000000000000000003500000000media-session-0.4.1/.gitlab/issue_templates/.gitkeep07070100000007000081A40000000000000000000000016178A88C00000307000000000000000000000000000000000000003F00000000media-session-0.4.1/.gitlab/issue_templates/bluetooth issue.mdIf you are filing this issue with a regular release please try master as it might already be fixed. If you can, test also with Pulseaudio and list `pulseaudio --version`. Bluetooth Radio, Bluetooth Headset, Desktop Environment, Distribution, Version (Bluez, Kernel, and PipeWire): ``` # run the following and paste output here lsusb; bluetoothctl devices; echo $XDG_SESSION_DESKTOP; grep PRETTY /etc/os-release; pipewire --version; bluetoothctl --version; uname -r ``` Description of Problem: How Reproducible: Steps to Reproduce: 1. 2. 3. Actual Results: Expected Results: Additional Info (as attachments): pw-dump output: `pw-dump -N > pw-dump.log` Bluetooth debug log https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Troubleshooting#bluetooth 07070100000008000081A40000000000000000000000016178A88C0000014B000000000000000000000000000000000000003500000000media-session-0.4.1/.gitlab/issue_templates/issue.mdIf you are filing this issue with a regular release please try master as it might already be fixed. Version, Distribution, Desktop Environment: Description of Problem: How Reproducible: Steps to Reproduce: 1. 2. 3. Actual Results: Expected Results: Additional Info Eg. `pw-dump -N > file` (As Attachment Please): 07070100000009000081A40000000000000000000000016178A88C00000D40000000000000000000000000000000000000002700000000media-session-0.4.1/CODE_OF_CONDUCT.md# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at pipewire-maintainers@lists.freedesktop.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq 0707010000000A000081A40000000000000000000000016178A88C000004C2000000000000000000000000000000000000001C00000000media-session-0.4.1/COPYINGCopyright © 2018 Wim Taymans Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- The above is the version of the MIT "Expat" License used by X.org: http://cgit.freedesktop.org/xorg/xserver/tree/COPYING 0707010000000B000081A40000000000000000000000016178A88C00000912000000000000000000000000000000000000001F00000000media-session-0.4.1/INSTALL.md## Building PipeWire Media Session uses a build tool called *Meson* as a basis for its build process. It's a tool with some resemblance to Autotools and CMake. Meson again generates build files for a lower level build tool called *Ninja*, working in about the same level of abstraction as more familiar GNU Make does. Meson uses a user-specified build directory and all files produced by Meson are in that build directory. This build directory will be called `builddir` in this document. Generate the build files for Ninja: ``` $ meson setup builddir ``` For distribution-specific build dependencies, please check our [CI pipeline](https://gitlab.freedesktop.org/pipewire/media-session/-/blob/master/.gitlab-ci.yml) (search for `FDO_DISTRIBUTION_PACKAGES`). Note that some dependencies are optional and depend on options passed to meson. Once this is done, the next step is to review the build options: ``` $ meson configure builddir ``` Define the installation prefix: ``` $ meson configure builddir -Dprefix=/usr # Default: /usr/local ``` PipeWire Media Session specific build options are listed in the "Project options" section. They are defined in `meson_options.txt`. Finally, invoke the build: ``` $ ninja -C builddir ``` Just to avoid any confusion: `autogen.sh` is a script invoked by *Jhbuild*, which orchestrates multi-component builds. ## Running If you want to run PipeWire Media Session without installing it on your system, there is a script that you can run. This puts you in an environment in which PipeWire Media Session can be run from the build directory. You can get into this environment with: ``` $ ./media-session-uninstalled.sh $ pipewire-media-session ``` This will use the default config file to configure and start PipeWire Media Session. You can also enable more debugging with the PIPEWIRE_DEBUG environment variable like so: ``` cd builddir/ PIPEWIRE_DEBUG="D" make run ``` You might have to stop the pipewire-media-session service that might have been started already, with: ``` systemctl --user stop pipewire-media-session.service ``` ## Installing Inside `builddir`, run: ``` sudo meson install ``` to install everything onto the system into the specified prefix. Some additional steps will have to be performed to integrate with the distribution as shown below. 0707010000000C000081A40000000000000000000000016178A88C0000006B000000000000000000000000000000000000001C00000000media-session-0.4.1/LICENSEAll PipeWire Media Session source files are licensed under the MIT License. (see file COPYING for details) 0707010000000D000081A40000000000000000000000016178A88C00000344000000000000000000000000000000000000001900000000media-session-0.4.1/NEWS# Media Session 0.4.1 (2021-10-27) Minor bugfix release functionally equivalent to the previous release. This release corrects a few leftovers pointing to the wrong repository and sets the minimum required PipeWire version. This way we fail early during meson setup rather than with linker errors during the build. # Media Session 0.4.0 (2021-10-21) This is the first standalone release of Media Session, previously part of the PipeWire repository. The binary name and service files remains unchanged as pipewire-media-session and pipewire-media-session.service. Media Session now suppports the MEDIA_SESSION_CONFIG_DIR environment variable as lookup location for configuration files. PIPEWIRE_CONFIG_DIR continues to be supported as before. The equivalent to pw-uninstalled.sh in this repository is media-session-uninstalled.sh. 0707010000000E000081A40000000000000000000000016178A88C0000045D000000000000000000000000000000000000001E00000000media-session-0.4.1/README.md# PipeWire Media Session PipeWire Media Session is an example session manager for [PipeWire](https://pipewire.org). Note that we recommend the use of [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) instead. ## Building and installation The preferred way to install PipeWire Media Session is to install it with your distribution package system. This ensures PipeWire Media Session is integrated into the rest of your system for the best experience. If you want to build and install PipeWire Media Session yourself, refer to [install](INSTALL.md) for instructions. ## Contributing PipeWire is Free Software and is developed in the open. It is mostly licensed under the [MIT license](COPYING). Check [LICENSE](LICENSE) for more details about the exceptions. Contributors are encouraged to submit merge requests or file bugs on [gitlab](https://gitlab.freedesktop.org/pipewire). Join us on IRC at #pipewire on [OFTC](https://www.oftc.net/). We adhere to the Contributor Covenant for our [code of conduct](CODE_OF_CONDUCT.md). [Donate using Liberapay](https://liberapay.com/PipeWire/donate). 0707010000000F000081ED0000000000000000000000016178A88C000001B3000000000000000000000000000000000000001F00000000media-session-0.4.1/autogen.sh#!/bin/sh # Only there to make jhbuild happy if [ -z "$MESON" ]; then MESON=$(which meson) fi if [ -z "$MESON" ]; then echo "error: Meson not found." echo "Install meson to configure and build PipeWire Media Session. If meson" \ "is already installed, set the environment variable MESON" \ "to the binary's path." exit 1; fi mkdir -p builddir $MESON setup "$@" builddir # use 'autogen.sh --reconfigure' to update 07070100000010000041ED0000000000000000000000026178A88C00000000000000000000000000000000000000000000001800000000media-session-0.4.1/doc07070100000011000081A40000000000000000000000016178A88C00000748000000000000000000000000000000000000002400000000media-session-0.4.1/doc/Doxyfile.inPROJECT_NAME = PipeWire PROJECT_NUMBER = @PACKAGE_VERSION@ OUTPUT_DIRECTORY = doc FULL_PATH_NAMES = NO JAVADOC_AUTOBRIEF = YES TAB_SIZE = 8 OPTIMIZE_OUTPUT_FOR_C = YES EXTRACT_ALL = YES EXTRACT_STATIC = YES FULL_PATH_NAMES = YES STRIP_FROM_PATH = @path_prefixes@ STRIP_FROM_INC_PATH = @path_prefixes@ SHOW_FILES = NO SHOW_INCLUDE_FILES = NO GENERATE_TODOLIST = NO GENERATE_TESTLIST = NO GENERATE_BUGLIST = NO QUIET = YES WARN_NO_PARAMDOC = YES HAVE_DOT = @HAVE_DOT@ INPUT = @inputs@ FILTER_PATTERNS = "*.c=@c_input_filter@" "*.h=@h_input_filter@" FILE_PATTERNS = "*.h" "*.c" RECURSIVE = YES REFERENCED_BY_RELATION = NO REFERENCES_RELATION = NO IGNORE_PREFIX = pw_ \ PW_ \ spa_ \ SPA_ GENERATE_TREEVIEW = YES SEARCHENGINE = NO GENERATE_LATEX = NO MACRO_EXPANSION = YES EXPAND_ONLY_PREDEF = YES PREDEFINED = PA_C_DECL_BEGIN= \ PA_C_DECL_END= \ __USE_ISOC11 \ SPA_EXPORT \ SPA_PRINTF_FUNC \ SPA_DEPRECATED \ SPA_SENTINEL \ SPA_UNUSED \ SPA_NORETURN \ SPA_RESTRICT HTML_EXTRA_STYLESHEET = @cssfiles@ MAX_INITIALIZER_LINES = 1 SORT_MEMBER_DOCS = NO CALL_GRAPH = NO CALLER_GRAPH = NO CLASS_GRAPH = NO COLLABORATION_GRAPH = NO GROUP_GRAPHS = NO INCLUDED_BY_GRAPH = NO INCLUDE_GRAPH = NO GRAPHICAL_HIERARCHY = NO DIRECTORY_GRAPH = NO TEMPLATE_RELATIONS = NO # Fix up some apparent Doxygen mis-parsing EXCLUDE_SYMBOLS = "desc" "methods" "msgid_plural" "n" "name" "props" "utils" "start" 07070100000012000081A40000000000000000000000016178A88C000001EC000000000000000000000000000000000000002300000000media-session-0.4.1/doc/custom.css:root { /* --page-background-color: #729fcf; */ --primary-color: #729fcf; --primary-dark-color: #729fcf; --header-background: #729fcf; --header-foreground: rgba(255, 255, 255, 0.7); --font-family: 'Source Sans Pro', 'Source Sans', sans-serif; } @media (prefers-color-scheme: light) { :root { --code-background: #f5f5f5; --code-foreground: #333333; --fragment-background: #f5f5f5; --fragment-foreground: #333333; --fragment-keyword: #c7254e; --fragment-link: #729fcf; } } 07070100000013000081A40000000000000000000000016178A88C00007B79000000000000000000000000000000000000002C00000000media-session-0.4.1/doc/doxygen-awesome.css/** Doxygen Awesome https://github.com/jothepro/doxygen-awesome-css MIT License Copyright (c) 2021 jothepro Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ :root { /* primary theme color. This will affect the entire websites color scheme: links, arrows, labels, ... */ --primary-color: #1982d2; --primary-dark-color: #00559f; --primary-light-color: #7aabd6; --primary-lighter-color: #cae1f1; --primary-lightest-color: #e9f1f8; /* page base colors */ --page-background-color: white; --page-foreground-color: #2c3e50; --page-secondary-foreground-color: #67727e; /* color for all separators on the website: hr, borders, ... */ --separator-color: #dedede; /* border radius for all rounded components. Will affect many components, like dropdowns, memitems, codeblocks, ... */ --border-radius-large: 8px; --border-radius-small: 4px; --border-radius-medium: 6px; /* default spacings. Most compontest reference these values for spacing, to provide uniform spacing on the page. */ --spacing-small: 5px; --spacing-medium: 10px; --spacing-large: 16px; /* default box shadow used for raising an element above the normal content. Used in dropdowns, Searchresult, ... */ --box-shadow: 0 2px 10px 0 rgba(0,0,0,.1); --odd-color: rgba(0,0,0,.03); /* font-families. will affect all text on the website * font-family: the normal font for text, headlines, menus * font-family-monospace: used for preformatted text in memtitle, code, fragments */ --font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif; --font-family-monospace: source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace; /* font sizes */ --page-font-size: 15.6px; --navigation-font-size: 14.4px; --code-font-size: 14.4px; /* affects code, fragment */ --title-font-size: 22px; /* content text properties. These only affect the page content, not the navigation or any other ui elements */ --content-line-height: 27px; /* The content is centered and constraint in it's width. To make the content fill the whole page, set the variable to auto.*/ --content-maxwidth: 900px; /* colors for various content boxes: @warning, @note, @deprecated @bug */ --warning-color: #fca49b; --warning-color-dark: #b61825; --warning-color-darker: #75070f; --note-color: rgba(255,229,100,.3); --note-color-dark: #c39900; --note-color-darker: #8d7400; --deprecated-color: rgb(214, 216, 224); --deprecated-color-dark: #5b6269; --deprecated-color-darker: #43454a; --bug-color: rgb(246, 208, 178); --bug-color-dark: #a53a00; --bug-color-darker: #5b1d00; --invariant-color: #b7f8d0; --invariant-color-dark: #00ba44; --invariant-color-darker: #008622; /* blockquote colors */ --blockquote-background: #f5f5f5; --blockquote-foreground: #727272; /* table colors */ --tablehead-background: #f1f1f1; --tablehead-foreground: var(--page-foreground-color); /* menu-display: block | none * Visibility of the top navigation on screens >= 768px. On smaller screen the menu is always visible. * `GENERATE_TREEVIEW` MUST be enabled! */ --menu-display: block; --menu-focus-foreground: var(--page-background-color); --menu-focus-background: var(--primary-color); --menu-selected-background: rgba(0,0,0,.05); --header-background: var(--page-background-color); --header-foreground: var(--page-foreground-color); /* searchbar colors */ --searchbar-background: var(--side-nav-background); --searchbar-foreground: var(--page-foreground-color); /* searchbar size * (`searchbar-width` is only applied on screens >= 768px. * on smaller screens the searchbar will always fill the entire screen width) */ --searchbar-height: 33px; --searchbar-width: 210px; /* code block colors */ --code-background: #f5f5f5; --code-foreground: var(--page-foreground-color); /* fragment colors */ --fragment-background: #282c34; --fragment-foreground: #ffffff; --fragment-keyword: #cc99cd; --fragment-keywordtype: #ab99cd; --fragment-keywordflow: #e08000; --fragment-token: #7ec699; --fragment-comment: #999999; --fragment-link: #98c0e3; --fragment-preprocessor: #65cabe; --fragment-linenumber-color: #cccccc; --fragment-linenumber-background: #35393c; --fragment-linenumber-border: #1f1f1f; --fragment-lineheight: 20px; /* sidebar navigation (treeview) colors */ --side-nav-background: #fbfbfb; --side-nav-foreground: var(--page-foreground-color); --side-nav-arrow-color: var(--page-background-color); /* height of an item in any tree / collapsible table */ --tree-item-height: 30px; } @media screen and (max-width: 767px) { :root { --page-font-size: 16px; --navigation-font-size: 16px; --code-font-size: 15px; /* affects code, fragment */ --title-font-size: 22px; } } @media (prefers-color-scheme: dark) { :root { --primary-color: #00559f; --primary-dark-color: #1982d2; --primary-light-color: #4779ac; --primary-lighter-color: #191e21; --primary-lightest-color: #191a1c; --box-shadow: 0 2px 10px 0 rgba(0,0,0,.35); --odd-color: rgba(0,0,0,.1); --menu-selected-background: rgba(0,0,0,.4); --page-background-color: #1C1D1F; --page-foreground-color: #d2dbde; --page-secondary-foreground-color: #859399; --separator-color: #000000; --side-nav-background: #252628; --code-background: #2a2c2f; --tablehead-background: #2a2c2f; --blockquote-background: #1f2022; --blockquote-foreground: #77848a; --warning-color: #b61825; --warning-color-dark: #510a02; --warning-color-darker: #f5b1aa; --note-color: rgb(255, 183, 0); --note-color-dark: #9f7300; --note-color-darker: #fff6df; --deprecated-color: rgb(88, 90, 96); --deprecated-color-dark: #262e37; --deprecated-color-darker: #a0a5b0; --bug-color: rgb(248, 113, 0); --bug-color-dark: #812a00; --bug-color-darker: #ffd3be; } } body { color: var(--page-foreground-color); background-color: var(--page-background-color); font-size: var(--page-font-size); } body, table, div, p, dl, #nav-tree .label, .title, .sm-dox a, .sm-dox a:hover, .sm-dox a:focus, #projectname, .SelectItem, #MSearchField, .navpath li.navelem a, .navpath li.navelem a:hover { font-family: var(--font-family); } h1, h2, h3, h4, h5 { margin-top: .9em; font-weight: 600; line-height: initial; } p, div, table, dl { font-size: var(--page-font-size); } a, a.el:visited, a.el:hover, a.el:focus, a.el:active { color: var(--primary-dark-color); } /* Title and top navigation */ #top { background: var(--header-background); border-bottom: 1px solid var(--separator-color); } @media screen and (min-width: 768px) { #top { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; } } #main-nav { flex-grow: 5; padding: var(--spacing-small) var(--spacing-medium); } #titlearea { width: auto; padding: var(--spacing-medium) var(--spacing-large); background: none; color: var(--header-foreground); border-bottom: none; } @media screen and (max-width: 767px) { #titlearea { padding-bottom: var(--spacing-small); } } #titlearea table tbody tr { height: auto !important; } #projectname { font-size: var(--title-font-size); font-weight: 600; } #projectnumber { font-family: inherit; font-size: 60%; } #projectbrief { font-family: inherit; font-size: 80%; } #projectlogo { vertical-align: middle; } #projectlogo img { max-height: calc(var(--title-font-size) * 2); margin-right: var(--spacing-small); } .sm-dox, .tabs, .tabs2, .tabs3 { background: none; padding: 0; } .tabs, .tabs2, .tabs3 { border-bottom: 1px solid var(--separator-color); margin-bottom: -1px; } @media screen and (max-width: 767px) { .sm-dox a span.sub-arrow { background: var(--code-background); } } @media screen and (min-width: 768px) { .sm-dox li, .tablist li { display: var(--menu-display); } .sm-dox a span.sub-arrow { border-color: var(--header-foreground) transparent transparent transparent; } .sm-dox a:hover span.sub-arrow { border-color: var(--menu-focus-foreground) transparent transparent transparent; } .sm-dox ul a span.sub-arrow { border-color: transparent transparent transparent var(--header-foreground); } .sm-dox ul a:hover span.sub-arrow { border-color: transparent transparent transparent var(--menu-focus-foreground); } } .sm-dox ul { background: var(--page-background-color); box-shadow: var(--box-shadow); border: 1px solid var(--separator-color); border-radius: var(--border-radius-medium) !important; padding: var(--spacing-small); animation: ease-out 150ms slideInMenu; } @keyframes slideInMenu { from { opacity: 0; transform: translate(0px, -2px); } to { opacity: 1; transform: translate(0px, 0px); } } .sm-dox ul a { color: var(--page-foreground-color); background: var(--page-background-color); font-size: var(--navigation-font-size); } .sm-dox>li>ul:after { border-bottom-color: var(--page-background-color) !important; } .sm-dox>li>ul:before { border-bottom-color: var(--separator-color) !important; } .sm-dox ul a:hover, .sm-dox ul a:active, .sm-dox ul a:focus { font-size: var(--navigation-font-size); color: var(--menu-focus-foreground); text-shadow: none; background-color: var(--menu-focus-background); border-radius: var(--border-radius-small) !important; } .sm-dox a, .sm-dox a:focus, .tablist li, .tablist li a, .tablist li.current a { text-shadow: none; background: transparent; background-image: none !important; color: var(--header-foreground); font-weight: normal; font-size: var(--navigation-font-size); } .sm-dox a:focus { outline: auto; } .sm-dox a:hover, .sm-dox a:active, .tablist li a:hover { text-shadow: none; font-weight: normal; background: var(--menu-focus-background); color: var(--menu-focus-foreground); border-radius: var(--border-radius-small) !important; font-size: var(--navigation-font-size); } .tablist li.current { border-radius: var(--border-radius-small); background: var(--menu-selected-background); } .tablist li { margin: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small); } .tablist a { padding: 0 var(--spacing-large); } /* Search box */ #MSearchBox { height: var(--searchbar-height); background: var(--searchbar-background); border-radius: var(--searchbar-height); border: 1px solid var(--separator-color); overflow: hidden; width: var(--searchbar-width); position: relative; box-shadow: none; display: block; margin-top: 0; } .left #MSearchSelect { left: 0; } .tabs .left #MSearchSelect { padding-left: 0; } .tabs #MSearchBox { position: absolute; right: var(--spacing-medium); } @media screen and (max-width: 767px) { .tabs #MSearchBox { position: relative; right: 0; margin-left: var(--spacing-medium); margin-top: 0; } } #MSearchSelectWindow, #MSearchResultsWindow { z-index: 9999; } #MSearchBox.MSearchBoxActive { border-color: var(--primary-color); box-shadow: inset 0 0 0 1px var(--primary-color); } #main-menu > li:last-child { margin-right: 0; } @media screen and (max-width: 767px) { #main-menu > li:last-child { height: 50px; } } #MSearchField { font-size: var(--navigation-font-size); height: calc(var(--searchbar-height) - 2px); background: transparent; width: calc(var(--searchbar-width) - 64px); } .MSearchBoxActive #MSearchField { color: var(--searchbar-foreground); } #MSearchSelect { top: calc(calc(var(--searchbar-height) / 2) - 11px); } .left #MSearchSelect { padding-left: 8px; } #MSearchBox span.left, #MSearchBox span.right { background: none; } #MSearchBox span.right { padding-top: calc(calc(var(--searchbar-height) / 2) - 12px); } .tabs #MSearchBox span.right { top: calc(calc(var(--searchbar-height) / 2) - 12px); } @keyframes slideInSearchResults { from { opacity: 0; transform: translate(0, 15px); } to { opacity: 1; transform: translate(0, 20px); } } #MSearchResultsWindow { left: auto !important; right: var(--spacing-medium); border-radius: var(--border-radius-large); border: 1px solid var(--separator-color); transform: translate(0, 20px); box-shadow: var(--box-shadow); animation: ease-out 280ms slideInSearchResults; background: var(--page-background-color); } iframe#MSearchResults { background: var(--page-background-color); margin: 4px; } #MSearchSelectWindow { border: 1px solid var(--separator-color); border-radius: var(--border-radius-medium); box-shadow: var(--box-shadow); background: var(--page-background-color); } #MSearchSelectWindow a.SelectItem { font-size: var(--navigation-font-size); line-height: var(--content-line-height); margin: 0 var(--spacing-small); border-radius: var(--border-radius-small); color: var(--page-foreground-color); } #MSearchSelectWindow a.SelectItem:hover { background: var(--menu-focus-background); color: var(--menu-focus-foreground); } @media screen and (max-width: 767px) { #MSearchBox { margin-top: var(--spacing-medium); margin-bottom: var(--spacing-medium); width: calc(100vw - 30px); } #main-menu > li:last-child { float: none !important; } #MSearchField { width: calc(100vw - 95px); } @keyframes slideInSearchResultsMobile { from { opacity: 0; transform: translate(0, 15px); } to { opacity: 1; transform: translate(0, 20px); } } #MSearchResultsWindow { left: var(--spacing-medium) !important; right: var(--spacing-medium); overflow: auto; transform: translate(0, 20px); animation: ease-out 280ms slideInSearchResultsMobile; } } /* Tree view */ #side-nav { padding: 0 !important; background: var(--side-nav-background); } @media screen and (max-width: 767px) { #side-nav { display: none; } #doc-content { margin-left: 0 !important; height: auto !important; padding-bottom: calc(2 * var(--spacing-large)); } } #nav-tree { background: transparent; } #nav-tree .label { font-size: var(--navigation-font-size); } #nav-tree .item { height: var(--tree-item-height); line-height: var(--tree-item-height); } #nav-sync { top: 12px !important; right: 12px; } #nav-tree .selected { text-shadow: none; background-image: none; background-color: transparent; box-shadow: inset 4px 0 0 0 var(--primary-dark-color); } #nav-tree a { color: var(--side-nav-foreground); } #nav-tree a:focus { outline-style: auto; } .arrow { color: var(--primary-light-color); font-family: serif; height: auto; text-align: right; } #nav-tree .arrow { opacity: 0; } #nav-tree div.item:hover .arrow, #nav-tree a:focus .arrow { opacity: 1; } #nav-tree .selected a { color: var(--primary-dark-color); font-weight: bolder; } .ui-resizable-e { background: var(--separator-color); width: 1px; } /* Contents */ div.header { border-bottom: 1px solid var(--separator-color); background-color: var(--page-background-color); background-image: none; } div.contents, div.header .title, div.header .summary { max-width: var(--content-maxwidth); } div.contents, div.header .title { line-height: initial; margin: calc(var(--spacing-medium) + .2em) auto var(--spacing-medium) auto; } div.header .summary { margin: var(--spacing-medium) auto 0 auto; } div.headertitle { padding: 0; } div.header .title { font-weight: 600; font-size: 210%; padding: var(--spacing-medium) var(--spacing-large); word-break: break-word; } div.header .summary { width: auto; display: block; float: none; padding: 0 var(--spacing-large); } td.memSeparator { border-color: var(--separator-color); } .mdescLeft, .mdescRight, .memItemLeft, .memItemRight, .memTemplItemLeft, .memTemplItemRight, .memTemplParams { background: var(--code-background); } .mdescRight { color: var(--page-secondary-foreground-color); } span.mlabel { background: var(--primary-color); border: none; padding: 4px 9px; border-radius: 12px; margin-right: var(--spacing-medium); } span.mlabel:last-of-type { margin-right: 2px; } div.contents { padding: 0 var(--spacing-large); } div.contents p, div.contents li { line-height: var(--content-line-height); } div.contents div.dyncontent { margin: var(--spacing-medium) 0; } @media (prefers-color-scheme: dark) { div.contents div.dyncontent img { filter: hue-rotate(180deg) invert(); } } h2.groupheader { border-bottom: 1px solid var(--separator-color); color: var(--page-foreground-color); } blockquote { padding: var(--spacing-small) var(--spacing-medium); background: var(--blockquote-background); color: var(--blockquote-foreground); border-left: 2px solid var(--blockquote-foreground); margin: 0; } blockquote p { margin: var(--spacing-small) 0 var(--spacing-medium) 0; } .paramname { color: var(--primary-dark-color); } .glow { text-shadow: 0 0 15px var(--primary-light-color) !important; } .alphachar a { color: var(--page-foreground-color); } /* Table of Contents */ div.toc { background-color: var(--side-nav-background); border: 1px solid var(--separator-color); border-radius: var(--border-radius-medium); box-shadow: var(--box-shadow); padding: 0 var(--spacing-large); margin: 0 0 var(--spacing-medium) var(--spacing-medium); } div.toc h3 { color: var(--side-nav-foreground); font-size: var(--navigation-font-size); margin: var(--spacing-large) 0; } div.toc li { font-size: var(--navigation-font-size); padding: 0; background: none; } div.toc li:before { content: '↓'; font-weight: 800; font-family: var(--font-family); margin-right: var(--spacing-small); color: var(--side-nav-foreground); opacity: .4; } div.toc ul li.level1 { margin: 0; } div.toc ul li.level2, div.toc ul li.level3 { margin-top: 0; } @media screen and (max-width: 767px) { div.toc { float: none; width: auto; margin: 0 0 var(--spacing-medium) 0; } } /* Code & Fragments */ code, div.fragment, pre.fragment { border-radius: var(--border-radius-small); border: none; overflow: hidden; } code { display: inline; background: var(--code-background); color: var(--code-foreground); padding: 2px 6px; word-break: break-word; } div.fragment, pre.fragment { margin: var(--spacing-medium) 0; padding: 14px 16px; background: var(--fragment-background); color: var(--fragment-foreground); overflow-x: auto; } @media screen and (max-width: 767px) { div.fragment, pre.fragment { border-top-right-radius: 0; border-bottom-right-radius: 0; } .contents > div.fragment, .textblock > div.fragment, .textblock > pre.fragment { margin: var(--spacing-medium) calc(0px - var(--spacing-large)); border-radius: 0; } .textblock li > .fragment { margin: var(--spacing-medium) calc(0px - var(--spacing-large)); } .memdoc li > .fragment { margin: var(--spacing-medium) calc(0px - var(--spacing-medium)); } .memdoc > div.fragment, .memdoc > pre.fragment, dl dd > div.fragment, dl dd pre.fragment { margin: var(--spacing-medium) calc(0px - var(--spacing-medium)); border-radius: 0; } } code, code a, pre.fragment, div.fragment, div.fragment .line, div.fragment span, div.fragment .line a, div.fragment .line span { font-family: var(--font-family-monospace); font-size: var(--code-font-size) !important; } div.line:after { margin-right: var(--spacing-medium); } div.fragment .line, pre.fragment { white-space: pre; word-wrap: initial; line-height: var(--fragment-lineheight); } div.fragment span.keyword { color: var(--fragment-keyword); } div.fragment span.keywordtype { color: var(--fragment-keywordtype); } div.fragment span.keywordflow { color: var(--fragment-keywordflow); } div.fragment span.stringliteral { color: var(--fragment-token) } div.fragment span.comment { color: var(--fragment-comment); } div.fragment a.code { color: var(--fragment-link); } div.fragment span.preprocessor { color: var(--fragment-preprocessor); } div.fragment span.lineno { display: inline-block; width: 27px; border-right: none; background: var(--fragment-linenumber-background); color: var(--fragment-linenumber-color); } div.fragment span.lineno a { background: none; color: var(--fragment-link); } div.fragment .line:first-child .lineno { box-shadow: -999999px 0px 0 999999px var(--fragment-linenumber-background), -999998px 0px 0 999999px var(--fragment-linenumber-border); } /* dl warning, attention, note, deprecated, bug, ... */ dl.warning, dl.attention, dl.note, dl.deprecated, dl.bug, dl.invariant, dl.pre { padding: var(--spacing-medium); margin: var(--spacing-medium) 0; color: var(--page-background-color); overflow: hidden; margin-left: 0; border-radius: var(--border-radius-small); } dl.section dd { margin-bottom: 2px; } dl.warning, dl.attention { background: var(--warning-color); border-left: 8px solid var(--warning-color-dark); color: var(--warning-color-darker); } dl.warning dt, dl.attention dt { color: var(--warning-color-dark); } dl.note { background: var(--note-color); border-left: 8px solid var(--note-color-dark); color: var(--note-color-darker); } dl.note dt { color: var(--note-color-dark); } dl.bug { background: var(--bug-color); border-left: 8px solid var(--bug-color-dark); color: var(--bug-color-darker); } dl.bug dt a { color: var(--bug-color-dark) !important; } dl.deprecated { background: var(--deprecated-color); border-left: 8px solid var(--deprecated-color-dark); color: var(--deprecated-color-darker); } dl.deprecated dt a { color: var(--deprecated-color-dark) !important; } dl.section dd, dl.bug dd, dl.deprecated dd { margin-inline-start: 0px; } dl.invariant, dl.pre { background: var(--invariant-color); border-left: 8px solid var(--invariant-color-dark); color: var(--invariant-color-darker); } /* memitem */ div.memdoc, div.memproto, h2.memtitle { box-shadow: none; background-image: none; border: none; } div.memdoc { padding: 0 var(--spacing-medium); background: var(--page-background-color); } h2.memtitle, div.memitem { border: 1px solid var(--separator-color); } div.memproto, h2.memtitle { background: var(--code-background); text-shadow: none; } h2.memtitle { font-weight: 500; font-family: monospace, fixed; border-bottom: none; border-top-left-radius: var(--border-radius-medium); border-top-right-radius: var(--border-radius-medium); word-break: break-all; } a:target + h2.memtitle, a:target + h2.memtitle + div.memitem { border-color: var(--primary-light-color); } a:target + h2.memtitle { box-shadow: -3px -3px 3px 0 var(--primary-lightest-color), 3px -3px 3px 0 var(--primary-lightest-color); } a:target + h2.memtitle + div.memitem { box-shadow: 0 0 10px 0 var(--primary-lighter-color); } div.memitem { border-top-right-radius: var(--border-radius-medium); border-bottom-right-radius: var(--border-radius-medium); border-bottom-left-radius: var(--border-radius-medium); overflow: hidden; display: block !important; } div.memdoc { border-radius: 0; } div.memproto { border-radius: 0 var(--border-radius-small) 0 0; overflow: auto; border-bottom: 1px solid var(--separator-color); padding: var(--spacing-medium); margin-bottom: -1px; } div.memtitle { border-top-right-radius: var(--border-radius-medium); border-top-left-radius: var(--border-radius-medium); } div.memproto table.memname { font-family: monospace, fixed; color: var(--page-foreground-color); } table.mlabels, table.mlabels > tbody { display: block; } td.mlabels-left { width: auto; } table.mlabels > tbody > tr:first-child { display: flex; justify-content: space-between; flex-wrap: wrap; } .memname, .memitem span.mlabels { margin: 0 } /* reflist */ dl.reflist { border-radius: var(--border-radius-medium); border: 1px solid var(--separator-color); overflow: hidden; padding: 0; } dl.reflist dt, dl.reflist dd { box-shadow: none; text-shadow: none; background-image: none; border: none; padding: 12px; } dl.reflist dt { border-radius: 0; background: var(--code-background); border-bottom: 1px solid var(--separator-color); color: var(--page-foreground-color) } dl.reflist dd { background: none; } /* Table */ table.markdownTable, table.fieldtable { width: 100%; border: 1px solid var(--separator-color); margin: var(--spacing-medium) 0; } table.fieldtable { box-shadow: none; border-radius: var(--border-radius-small); } th.markdownTableHeadLeft, th.markdownTableHeadRight, th.markdownTableHeadCenter, th.markdownTableHeadNone { background: var(--tablehead-background); color: var(--tablehead-foreground); font-weight: 600; } table.markdownTable td, table.markdownTable th, table.fieldtable dt { border: 1px solid var(--separator-color); padding: var(--spacing-small) var(--spacing-medium); } table.fieldtable th { font-size: var(--page-font-size); font-weight: 600; background-image: none; background-color: var(--tablehead-background); color: var(--tablehead-foreground); border-bottom: 1px solid var(--separator-color); } .fieldtable td.fieldtype, .fieldtable td.fieldname { border-bottom: 1px solid var(--separator-color); border-right: 1px solid var(--separator-color); } .fieldtable td.fielddoc { border-bottom: 1px solid var(--separator-color); } .memberdecls td.glow, .fieldtable tr.glow { background-color: var(--primary-light-color); box-shadow: 0 0 15px var(--primary-lighter-color); } table.memberdecls { display: block; overflow-x: auto; overflow-y: hidden; } /* Horizontal Rule */ hr { margin-top: var(--spacing-large); margin-bottom: var(--spacing-large); border-top:1px solid var(--separator-color); } .contents hr { box-shadow: var(--content-maxwidth) 0 0 0 var(--separator-color), calc(0px - var(--content-maxwidth)) 0 0 0 var(--separator-color); } .contents img { max-width: 100%; } /* Directories */ div.directory { border-top: 1px solid var(--separator-color); border-bottom: 1px solid var(--separator-color); width: auto; } table.directory { font-family: var(--font-family); font-size: var(--page-font-size); font-weight: normal; } .directory td.entry { padding: var(--spacing-small); display: flex; align-items: center; } .directory tr.even { background-color: var(--odd-color); } .icona { width: auto; height: auto; margin: 0 var(--spacing-small); } .icon { background: var(--primary-dark-color); width: 18px; height: 18px; line-height: 18px; } .iconfopen, .icondoc, .iconfclosed { background-position: center; margin-bottom: 0; } .icondoc { filter: saturate(0.2); } @media screen and (max-width: 767px) { div.directory { margin-left: calc(0px - var(--spacing-medium)); margin-right: calc(0px - var(--spacing-medium)); } } @media (prefers-color-scheme: dark) { .iconfopen, .iconfclosed { filter: hue-rotate(180deg) invert(); } } /* Class list */ .classindex dl.odd { background: var(--odd-color); border-radius: var(--border-radius-small); } @media screen and (max-width: 767px) { .classindex { margin: 0 calc(0px - var(--spacing-small)); } } /* Footer and nav-path */ #nav-path { margin-bottom: -1px; width: 100%; } #nav-path ul { background-image: none; background: var(--page-background-color); border: none; border-top: 1px solid var(--separator-color); border-bottom: 1px solid var(--separator-color); font-size: var(--navigation-font-size); } img.footer { width: 60px; } .navpath li.footer { color: var(--page-secondary-foreground-color); } address.footer { margin-bottom: var(--spacing-large); } #nav-path li.navelem { background-image: none; display: flex; align-items: center; } .navpath li.navelem a { text-shadow: none; display: inline-block; color: var(--primary-dark-color) } li.navelem { padding: 0; margin-left: -8px; } li.navelem:first-child { margin-left: var(--spacing-large); } li.navelem:first-child:before { display: none; } #nav-path li.navelem:after { content: ''; border: 5px solid var(--page-background-color); border-bottom-color: transparent; border-right-color: transparent; border-top-color: transparent; transform: scaleY(4.2); z-index: 10; margin-left: 6px; } #nav-path li.navelem:before { content: ''; border: 5px solid var(--separator-color); border-bottom-color: transparent; border-right-color: transparent; border-top-color: transparent; transform: scaleY(3.2); margin-right: var(--spacing-small); } @media (prefers-color-scheme: dark) { #nav-path li.navelem:after { text-shadow: 3px 0 0 var(--separator-color), 8px 0 6px rgba(0,0,0,0.4); } } .navpath li.navelem a:hover { color: var(--primary-color); } 07070100000014000081A40000000000000000000000016178A88C000009F9000000000000000000000000000000000000002200000000media-session-0.4.1/doc/index.dox/** \mainpage PipeWire Media Session PipeWire Media Session is the reference/example session manager provided by the [PipeWire](https://pipewire.org) project. On startup, Media Session reads the `media-session.conf` configuration file to configure itself. The following directories are searched for this file: - in `$XDG_CONFIG_HOME/pipewire/media-session.d/` (usually `$HOME/.config/pipewire/media-session.d/`) - `$sysconfdir/pipewire/media-session.d` (usually `/etc/pipewire/media-session.d/`) - `$datadir/pipewire/media-session.d/` (usually `/usr/share/pipewire/media-session.d/`) The environment variable `MEDIA_SESSION_CONFIG_DIR` can be used to specify an alternative config directory. ## Access management The \ref page_media_session_module_access_flatpak module handles clients that have \ref PW_KEY_ACCESS set to "flatpak". Other clients are ignored. The module sets the permissions of all objects to `RX`. This limits the flatpaks from doing modifications to other objects. Because this will also set the core object permission `R`, the client will resume with the new permissions. `pipewire-media-session` implements \ref PW_KEY_MEDIA_CATEGORY type "Manager" applications by simply setting the client permissions to ALL. No additional checks are performed yet. ## Modules List of Media Session modules: - \subpage page_media_session_module_access_flatpak - \subpage page_media_session_module_access_portal - \subpage page_media_session_module_alsa_endpoint - \subpage page_media_session_module_alsa_midi - \subpage page_media_session_module_alsa_monitor - \subpage page_media_session_module_bluez_autoswitch - \subpage page_media_session_module_bluez_endpoint - \subpage page_media_session_module_bluez_monitor - \subpage page_media_session_module_default_nodes - \subpage page_media_session_module_default_profile - \subpage page_media_session_module_default_routes - \subpage page_media_session_module_libcamera_monitor - \subpage page_media_session_module_logind - \subpage page_media_session_module_metadata - \subpage page_media_session_module_no_dsp - \subpage page_media_session_module_policy_endpoint - \subpage page_media_session_module_policy_node - \subpage page_media_session_module_restore_stream - \subpage page_media_session_module_session_manager - \subpage page_media_session_module_stream_endpoint - \subpage page_media_session_module_stream_follow_default - \subpage page_media_session_module_suspend_node - \subpage page_media_session_module_v4l2_endpoint - \subpage page_media_session_module_v4l2_monitor */ 07070100000015000081ED0000000000000000000000016178A88C000005CD000000000000000000000000000000000000002A00000000media-session-0.4.1/doc/input-filter-h.sh#!/bin/bash # # Doxygen input filter, which tries to fix documentation of callback # method macros. # # This is used for .h files. # FILENAME="$1" # Add \ingroup commands for the file, for each \addgroup in it BASEFILE=$(echo "$FILENAME" | sed -e 's@.*src/pipewire/@pipewire/@; s@.*spa/include/spa/@spa/@; s@.*src/test/@test/@;') echo "/** \file" echo "\`$BASEFILE\`" sed -n -e '/.*\\addtogroup [a-zA-Z0-9_].*/ { s/.*addtogroup /\\ingroup /; p; }' < "$FILENAME" | sort | uniq echo " */" # Add \sa and \copydoc for (struct *methods) callback macros. # #define pw_core_add_listener(...) pw_core_method(c,add_listener,...) -> add \sa and \copydoc # #define spa_system_read(...) spa_system_method_r(c,read,...) -> add \sa and \copydoc # # Also: # Ensure all macros are included (also those defined inside a struct), # by adding /** \ingroup XXX */ before each definition. # Also ensure all opaque structs get included. sed -e 's@^\(#define .*[[:space:]]\)\(.*_method\)\((.,[[:space:]]*\)\([a-z_]\+\)\(.*)[[:space:]]*\)$@\1\2\3\4\5 /**< \\copydoc \2s.\4\n\n\\sa \2s.\4 */@;' \ -e 's@^\(#define .*[[:space:]]\)\(.*_method\)\(_[rvs](.,[[:space:]]*\)\([a-z_]\+\)\(.*)[[:space:]]*\)$@\1\2\3\4\5 /**< \\copydoc \2s.\4\n\n\\sa \2s.\4 */@;' \ -e '/\\addtogroup/ { h; s@.*\\addtogroup \(.*\).*@/** \\ingroup \1 */@; x; }' \ -e '/#define \(PW\|SPA\)_[A-Z].*[^\\][ ]*$/ { x; p; x; }' \ -e 's@^\([ ]*struct \)\([a-zA-Z0-9_]*\)\(;.*\)$@/** \\struct \2 */\n\1\2\3@;' \ < "$FILENAME" 07070100000016000081ED0000000000000000000000016178A88C0000012B000000000000000000000000000000000000002800000000media-session-0.4.1/doc/input-filter.sh#!/bin/bash # # Doxygen input filter that adds \privatesection to all files, # and removes macros. # # This is used for .c files, and causes Doxygen to not include # any symbols from them, unless they also appeared in a header file. # echo -n "/** \privatesection */ " sed -e 's/#define.*//' < "$1" 07070100000017000081A40000000000000000000000016178A88C00000806000000000000000000000000000000000000002400000000media-session-0.4.1/doc/meson.builddoxyfile_conf = configuration_data() doxyfile_conf.set('PACKAGE_NAME', meson.project_name()) doxyfile_conf.set('PACKAGE_VERSION', meson.project_version()) doxyfile_conf.set('top_srcdir', meson.project_source_root()) doxyfile_conf.set('top_builddir', meson.project_build_root()) dot_found = find_program('dot', required: false).found() summary({'dot (used with doxygen)': dot_found}, bool_yn: true, section: 'Optional programs') if dot_found doxyfile_conf.set('HAVE_DOT', 'YES') else doxyfile_conf.set('HAVE_DOT', 'NO') endif # Note: order here is how doxygen will expose the pages in the sidebar # api-tree.dox should be first to determine ordering of Modules. extra_docs = [ 'index.dox', ] inputs = [] foreach extra : extra_docs inputs += meson.project_source_root() / 'doc' / extra endforeach foreach h : media_session_sources inputs += meson.project_source_root() / 'src' / h endforeach path_prefixes = [ meson.project_source_root() / 'src', meson.project_source_root(), ] cssfiles = [ meson.project_source_root() / 'doc' / 'doxygen-awesome.css', meson.project_source_root() / 'doc' / 'custom.css' ] doxyfile_conf.set('inputs', ' '.join(inputs)) doxyfile_conf.set('cssfiles', ' '.join(cssfiles)) doxyfile_conf.set('path_prefixes', ' '.join(path_prefixes)) doxyfile_conf.set('c_input_filter', meson.project_source_root() / 'doc' / 'input-filter.sh') doxyfile_conf.set('h_input_filter', meson.project_source_root() / 'doc' / 'input-filter-h.sh') doxyfile = configure_file(input: 'Doxyfile.in', output: 'Doxyfile', configuration: doxyfile_conf) docdir = get_option('docdir') if docdir == '' docdir = media_session_datadir / 'doc' / meson.project_name() endif html_target = custom_target('pipewire-docs', input: [ doxyfile ] + inputs + cssfiles, output: [ 'html' ], command: [ doxygen, doxyfile ], install: true, install_dir: docdir) 07070100000018000081ED0000000000000000000000016178A88C00000469000000000000000000000000000000000000003100000000media-session-0.4.1/media-session-uninstalled.sh#!/usr/bin/env bash set -e # This is unset by meson # shellcheck disable=SC2157 if [ -z "@MESON@" ]; then SOURCEDIR="@MESON_SOURCE_ROOT@" BUILDDIR="@MESON_BUILD_ROOT@" else SOURCEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" BUILDDIR=$(find "${SOURCEDIR}" -maxdepth 2 -name build.ninja -printf "%h\n" -quit 2>/dev/null || echo "${SOURCEDIR}/builddir") fi while getopts ":b:v:" opt; do case ${opt} in b) BUILDDIR=${OPTARG} ;; v) VERSION=${OPTARG} echo "Version: ${VERSION}" ;; \?) echo "Invalid option: -${OPTARG}" exit 1 ;; :) echo "Option -${OPTARG} requires an argument" exit 1 ;; esac done shift $((OPTIND-1)) if [ ! -d "${BUILDDIR}" ]; then echo "Invalid build directory: ${BUILDDIR}" exit 1 fi # the config file read by the daemon export MEDIA_SESSION_CONFIG_DIR="${BUILDDIR}/media-session.d" export PATH="${BUILDDIR}/src:${PATH}" export PW_UNINSTALLED=1 export PKG_CONFIG_PATH="${BUILDDIR}/meson-uninstalled/:${PKG_CONFIG_PATH}" if [ -z "$1" ]; then # FIXME: find a nice, shell-neutral way to specify a prompt ${SHELL} else exec "$@" fi 07070100000019000041ED0000000000000000000000026178A88C00000000000000000000000000000000000000000000002400000000media-session-0.4.1/media-session.d0707010000001A000081A40000000000000000000000016178A88C00001474000000000000000000000000000000000000003600000000media-session-0.4.1/media-session.d/alsa-monitor.conf# ALSA monitor config file for PipeWire version @VERSION@ # # # Copy and edit this file in @MEDIA_SESSION_CONFIG_DIR@/media-session.d/ # for system-wide changes or in # ~/.config/pipewire/media-session.d/ for local changes. properties = { # Create a JACK device. This is not enabled by default because # it requires that the PipeWire JACK replacement libraries are # not used by the session manager, in order to be able to # connect to the real JACK server. #alsa.jack-device = false # Reserve devices. #alsa.reserve = true } rules = [ # An array of matches/actions to evaluate. { # Rules for matching a device or node. Each dictionary in this array # specifies the property to match as key and a string or regex match # as value. A successful match requires all dictionary keys (i.e. # properties) to match. # # Actions are are executed for the object if at least one successful # match exists. # # Regular expressions are prefixed with the ~ (tilde) character, # otherwise a standard string comparison is used. # The special value "null" matches against empty properties. matches = [ { # This matches all cards. These are regular expressions # so "." matches one character and ".*" matches many. device.name = "~alsa_card.*" } ] actions = { # Actions can update properties on the matched object. update-props = { # Use ALSA-Card-Profile devices. They use UCM or # the profile configuration to configure the device # and mixer settings. api.alsa.use-acp = true # Use UCM instead of profile when available. Can be # disabled to skip trying to use the UCM profile. #api.alsa.use-ucm = true # Don't use the hardware mixer for volume control. It # will only use software volume. The mixer is still used # to mute unused paths based on the selected port. #api.alsa.soft-mixer = false # Ignore decibel settings of the driver. Can be used to # work around buggy drivers that report wrong values. #api.alsa.ignore-dB = false # The profile set to use for the device. Usually this is # "default.conf" but can be changed with a udev rule # or here. #device.profile-set = "profileset-name.conf" # The default active profile. Is by default set to "Off". #device.profile = "default profile name" # Automatically select the best profile. This is the # highest priority available profile. This is disabled # here and instead implemented in the session manager # where it can save and load previous preferences. api.acp.auto-profile = false # Automatically switch to the highest priority available # port. This is disabled here and implemented in the # session manager instead. api.acp.auto-port = false # Other properties can be set here. #device.nick = "My Device" } } } { matches = [ { # Matches all sources. These are regular expressions # so "." matches one character and ".*" matches many. node.name = "~alsa_input.*" } { # Matches all sinks. node.name = "~alsa_output.*" } ] actions = { update-props = { #node.nick = "My Node" #node.nick = null #priority.driver = 100 #priority.session = 100 node.pause-on-idle = false #resample.quality = 4 #channelmix.normalize = false #channelmix.mix-lfe = false #audio.channels = 2 #audio.format = "S16LE" #audio.rate = 44100 #audio.position = "FL,FR" #session.suspend-timeout-seconds = 5 # 0 disables suspend #monitor.channel-volumes = false #latency.internal.rate = 0 # internal latency in samples #latency.internal.ns = 0 # internal latency in nanoseconds #api.alsa.period-size = 1024 #api.alsa.headroom = 0 #api.alsa.start-delay = 0 #api.alsa.disable-mmap = false #api.alsa.disable-batch = false #api.alsa.use-chmap = false #iec958.codecs = [ PCM DTS AC3 MPEG MPEG2-AAC EAC3 TrueHD DTS-HD ] } } } ] 0707010000001B000081A40000000000000000000000016178A88C000014F8000000000000000000000000000000000000003700000000media-session-0.4.1/media-session.d/bluez-monitor.conf# Bluez monitor config file for PipeWire version @VERSION@ # # # Copy and edit this file in @MEDIA_SESSION_CONFIG_DIR@/media-session.d/ # for system-wide changes or in # ~/.config/pipewire/media-session.d/ for local changes. properties = { # These features do not work on all headsets, so they are enabled # by default based on the hardware database. They can also be # forced on/off for all devices by the following options: #bluez5.enable-sbc-xq = true #bluez5.enable-msbc = true #bluez5.enable-hw-volume = true #bluez5.enable-faststream = true # See bluez-hardware.conf for the hardware database. # Enabled headset roles (default: [ hsp_hs hfp_ag ]), this # property only applies to native backend. Currently some headsets # (Sony WH-1000XM3) are not working with both hsp_ag and hfp_ag # enabled, disable either hsp_ag or hfp_ag to work around it. # # Supported headset roles: hsp_hs (HSP Headset), # hsp_ag (HSP Audio Gateway), # hfp_hf (HFP Hands-Free), # hfp_ag (HFP Audio Gateway) #bluez5.headset-roles = [ hsp_hs hsp_ag hfp_hf hfp_ag ] # Enabled A2DP codecs (default: all). #bluez5.codecs = [ sbc sbc_xq aac ldac aptx aptx_hd aptx_ll aptx_ll_duplex faststream faststream_duplex ] # HFP/HSP backend (default: native). # Available values: any, none, hsphfpd, ofono, native #bluez5.hfphsp-backend = native # Properties for the A2DP codec configuration #bluez5.default.rate = 48000 #bluez5.default.channels = 2 # Register dummy AVRCP player, required for AVRCP volume function. # Disable if you are running mpris-proxy or equivalent. #bluez5.dummy-avrcp-player = true } rules = [ # An array of matches/actions to evaluate. { # Rules for matching a device or node. It is an array of # properties that all need to match the regexp. If any of the # matches work, the actions are executed for the object. matches = [ { # This matches all cards. device.name = "~bluez_card.*" } ] actions = { # Actions can update properties on the matched object. update-props = { # Auto-connect device profiles on start up or when only partial # profiles have connected. Disabled by default if the property # is not specified. #bluez5.auto-connect = [ # hfp_hf # hsp_hs # a2dp_sink # hfp_ag # hsp_ag # a2dp_source #] bluez5.auto-connect = [ hfp_hf hsp_hs a2dp_sink ] # Hardware volume control (default: all) #bluez5.hw-volume = [ # hfp_hf # hsp_hs # a2dp_sink # hfp_ag # hsp_ag # a2dp_source #] # LDAC encoding quality # Available values: auto (Adaptive Bitrate, default) # hq (High Quality, 990/909kbps) # sq (Standard Quality, 660/606kbps) # mq (Mobile use Quality, 330/303kbps) #bluez5.a2dp.ldac.quality = auto # AAC variable bitrate mode # Available values: 0 (cbr, default), 1-5 (quality level) #bluez5.a2dp.aac.bitratemode = 0 # Profile connected first # Available values: a2dp-sink (default), headset-head-unit #bluez5.profile = a2dp-sink # A2DP <-> HFP profile auto-switching (when device is default output) # Available values: false, role (default), true # 'role' will switch the profile if the recording application # specifies Communication (or "phone" in PA) as the stream role. #bluez5.autoswitch-profile = role } } } { matches = [ { # Matches all sources. node.name = "~bluez_input.*" } { # Matches all sinks. node.name = "~bluez_output.*" } ] actions = { update-props = { #node.nick = "My Node" #node.nick = null #priority.driver = 100 #priority.session = 100 node.pause-on-idle = false #resample.quality = 4 #channelmix.normalize = false #channelmix.mix-lfe = false #session.suspend-timeout-seconds = 5 # 0 disables suspend #monitor.channel-volumes = false # A2DP source role, "input" or "playback" # Defaults to "playback", playing stream to speakers # Set to "input" to use as an input for apps #bluez5.a2dp-source-role = input } } } ] 0707010000001C000081A40000000000000000000000016178A88C00000F7C000000000000000000000000000000000000003700000000media-session-0.4.1/media-session.d/media-session.conf# Media Session config file for PipeWire version @VERSION@ # # # Copy and edit this file in @MEDIA_SESSION_CONFIG_DIR@/media-session.d/ # for system-wide changes or in # ~/.config/pipewire/media-session.d/ for local changes. context.properties = { # Properties to configure the session and some # modules. #mem.mlock-all = false #support.dbus = true #log.level = 2 #alsa.seq.name = Midi-Bridge #default-profile.restore-bluetooth = false } context.spa-libs = { # Mapping from factory name to library. api.bluez5.* = bluez5/libspa-bluez5 api.alsa.* = alsa/libspa-alsa api.v4l2.* = v4l2/libspa-v4l2 api.libcamera.* = libcamera/libspa-libcamera } context.modules = [ #{ name = <module-name> # [ args = { <key> = <value> ... } ] # [ flags = [ [ ifexists ] [ nofail ] ] #} # # Loads a module with the given parameters. # If ifexists is given, the module is ignored when it is not found. # If nofail is given, module initialization failures are ignored. # # Uses RTKit to boost the data thread priority. { name = libpipewire-module-rtkit args = { #nice.level = -11 #rt.prio = 88 #rt.time.soft = 2000000 #rt.time.hard = 2000000 } flags = [ ifexists nofail ] } # The native communication protocol. { name = libpipewire-module-protocol-native } # Allows creating nodes that run in the context of the # client. Is used by all clients that want to provide # data to PipeWire. { name = libpipewire-module-client-node } # Allows creating devices that run in the context of the # client. Is used by the session manager. { name = libpipewire-module-client-device } # Makes a factory for wrapping nodes in an adapter with a # converter and resampler. { name = libpipewire-module-adapter } # Allows applications to create metadata objects. It creates # a factory for Metadata objects. { name = libpipewire-module-metadata } # Provides factories to make session manager objects. { name = libpipewire-module-session-manager } ] session.modules = { # These are the modules that are enabled when a file with # the key name is found in the media-session.d config directory. # the default bundle is always enabled. default = [ flatpak # manages flatpak access portal # manage portal permissions v4l2 # video for linux udev detection #libcamera # libcamera udev detection suspend-node # suspend inactive nodes policy-node # configure and link nodes #metadata # export metadata API #default-nodes # restore default nodes #default-profile # restore default profiles #default-routes # restore default route #streams-follow-default # move streams when default changes #alsa-no-dsp # do not configure audio nodes in DSP mode #alsa-seq # alsa seq midi support #alsa-monitor # alsa udev detection #bluez5 # bluetooth support #bluez5-autoswitch # automatic bluetooth HSP/HFP profile switch #restore-stream # restore stream settings #logind # systemd-logind seat support ] with-audio = [ metadata default-nodes default-profile default-routes alsa-seq alsa-monitor ] with-alsa = [ with-audio ] with-jack = [ with-audio ] with-pulseaudio = [ with-audio bluez5 bluez5-autoswitch logind restore-stream streams-follow-default ] } 0707010000001D000081A40000000000000000000000016178A88C000002E2000000000000000000000000000000000000003000000000media-session-0.4.1/media-session.d/meson.buildconf_config = configuration_data() conf_config.set('VERSION', '@0@'.format(media_session_version)) conf_config.set('MEDIA_SESSION_CONFIG_DIR', media_session_configdir) conf_files = [ [ 'bluez-monitor.conf', 'bluez-monitor.conf' ], [ 'v4l2-monitor.conf', 'v4l2-monitor.conf' ], [ 'media-session.conf', 'media-session.conf' ], [ 'alsa-monitor.conf', 'alsa-monitor.conf' ], ] foreach opt : get_option('with-module-sets') conf_files += [ [ 'with-module-set.in', 'with-@0@'.format(opt) ] ] endforeach foreach c : conf_files configure_file(input : c.get(0), output : c.get(1), configuration : conf_config, install_dir : media_session_confdatadir / 'media-session.d') endforeach 0707010000001E000081A40000000000000000000000016178A88C0000060B000000000000000000000000000000000000003600000000media-session-0.4.1/media-session.d/v4l2-monitor.conf# V4L2 monitor config file for PipeWire version @VERSION@ # # # Copy and edit this file in @MEDIA_SESSION_CONFIG_DIR@/media-session.d/ # for system-wide changes or in # ~/.config/pipewire/media-session.d/ for local changes. properties = { } rules = [ # An array of matches/actions to evaluate. { # Rules for matching a device or node. It is an array of # properties that all need to match the regexp. If any of the # matches work, the actions are executed for the object. matches = [ { # This matches all devices. device.name = "~v4l2_device.*" } ] actions = { # Actions can update properties on the matched object. update-props = { #device.nick = "My Device" } } } { matches = [ { # Matches all sources. node.name = "~v4l2_input.*" } { # Matches all sinks. node.name = "~v4l2_output.*" } ] actions = { update-props = { #node.nick = "My Node" #node.nick = null #priority.driver = 100 #priority.session = 100 node.pause-on-idle = false #session.suspend-timeout-seconds = 5 # 0 disables suspend } } } ] 0707010000001F000081A40000000000000000000000016178A88C000000DE000000000000000000000000000000000000003700000000media-session-0.4.1/media-session.d/with-module-set.in# This file is part of Media Session @VERSION@ # # Add this file to @MEDIA_SESSION_CONFIG_DIR@/media-session.d/ # to load the corresponding module list in the in the media-session.conf # session.modules[<filename>] array. 07070100000020000081A40000000000000000000000016178A88C00001295000000000000000000000000000000000000002000000000media-session-0.4.1/meson.buildproject('media-session', ['c'], version : '0.4.1', license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ], meson_version : '>= 0.56.0', default_options : [ 'warning_level=3', 'c_std=gnu99', 'b_pie=true', #'b_sanitize=address,undefined', 'buildtype=debugoptimized' ]) media_session_version = meson.project_version() prefix = get_option('prefix') media_session_bindir = prefix / get_option('bindir') media_session_datadir = prefix / get_option('datadir') media_session_libdir = prefix / get_option('libdir') media_session_libexecdir = prefix / get_option('libexecdir') media_session_localedir = prefix / get_option('localedir') media_session_sysconfdir = prefix / get_option('sysconfdir') # For historical reasons, we drop our data into the pipewire directories media_session_configdir = media_session_sysconfdir / 'pipewire' media_session_confdatadir = media_session_datadir / 'pipewire' gnome = import('gnome') pkgconfig = import('pkgconfig') cc = meson.get_compiler('c') common_flags = [ '-fvisibility=hidden', '-Werror=suggest-attribute=format', '-Wsign-compare', '-Wpointer-arith', '-Wpointer-sign', '-Wformat', '-Wformat-security', '-Wimplicit-fallthrough', '-Wmissing-braces', '-Wtype-limits', '-Wvariadic-macros', '-Wno-missing-field-initializers', '-Wno-unused-parameter', '-Wno-pedantic', '-Wold-style-declaration', '-Wunused-result', ] cc_flags = common_flags + [ '-D_GNU_SOURCE', ] add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c') cdata = configuration_data() cdata.set_quoted('MEDIA_SESSION_CONFDATADIR', media_session_confdatadir) cdata.set_quoted('LOCALEDIR', media_session_localedir) cdata.set_quoted('LIBDIR', media_session_libdir) cdata.set_quoted('GETTEXT_PACKAGE', meson.project_name()) cdata.set_quoted('PACKAGE', 'media-session') cdata.set_quoted('PACKAGE_NAME', '"media-session"') cdata.set_quoted('PACKAGE_STRING', 'media-session @0@'.format(media_session_version)) cdata.set_quoted('PACKAGE_TARNAME', 'media-session') cdata.set_quoted('PACKAGE_URL', '"https://pipewire.org"') cdata.set_quoted('PACKAGE_VERSION', media_session_version) pipewire_dep = dependency('libpipewire-0.3', version: '>= 0.3.39') systemd = dependency('systemd', required: get_option('systemd')) systemd_dep = dependency('libsystemd',required: get_option('systemd')) summary({'systemd conf data': systemd.found()}, bool_yn: true) summary({'libsystemd': systemd_dep.found()}, bool_yn: true) if systemd.found() and systemd_dep.found() cdata.set('HAVE_SYSTEMD', 1) endif configinc = include_directories('.') # Find dependencies mathlib = cc.find_library('m', required : false) rt_lib = cc.find_library('rt', required : false) # clock_gettime dl_lib = cc.find_library('dl', required : false) pthread_lib = dependency('threads') dbus_dep = dependency('dbus-1') libinotify_dep = (build_machine.system() == 'freebsd' ? dependency('libinotify', required: true) : dependency('', required: false)) # On FreeBSD, libintl library is required for gettext libintl_dep = dependency('intl', required: false) if not libintl_dep.found() libintl_dep = cc.find_library('intl', required: false) endif summary({'intl support': libintl_dep.found()}, bool_yn: true) alsa_dep = dependency('alsa', version : '>=1.1.7') subdir('po') subdir('src') subdir('media-session.d') if systemd.found() subdir('systemd') endif configure_file(output : 'config.h', configuration : cdata) doxygen = find_program('doxygen', required : get_option('docs')) if doxygen.found() subdir('doc') endif conf_uninstalled = configuration_data() conf_uninstalled.set('MESON', '') conf_uninstalled.set('MESON_SOURCE_ROOT', meson.project_source_root()) conf_uninstalled.set('MESON_BUILD_ROOT', meson.project_build_root()) ms_uninstalled = configure_file( input : 'media-session-uninstalled.sh', output : 'media-session-uninstalled.sh.in', configuration : conf_uninstalled, ) media_session_uninstalled = custom_target('media-session-uninstalled', output : 'media-session-uninstalled.sh', input : ms_uninstalled, build_by_default : true, command : ['cp', '@INPUT@', '@OUTPUT@'], ) run_target('media-session-uninstalled', command : [media_session_uninstalled, '-b@0@'.format(meson.project_build_root()), '-v@0@'.format(media_session_version)] ) if meson.version().version_compare('>=0.58.0') devenv = environment() builddir = meson.current_build_dir() srcdir = meson.current_source_dir() devenv.set('MEDIA_SESSION_CONFIG_DIR', builddir / 'media-session.d') devenv.set('PW_UNINSTALLED', '1') meson.add_devenv(devenv) endif 07070100000021000081A40000000000000000000000016178A88C0000051D000000000000000000000000000000000000002600000000media-session-0.4.1/meson_options.txtoption('docdir', type : 'string', description : 'Directory for installing documentation to (defaults to pipewire_datadir/doc/meson.project_name() )') option('docs', description: 'Build documentation', type: 'feature', value: 'disabled') option('tests', description: 'Build tests', type: 'feature', value: 'enabled', yield : true) option('installed_tests', description: 'Install manual and automated test executables', type: 'feature', value: 'disabled') option('systemd', description: 'Enable systemd integration', type: 'feature', value: 'auto') option('systemd-system-service', description: 'Install systemd system service file', type: 'feature', value: 'disabled') option('systemd-user-service', description: 'Install systemd user service file (ignored without systemd)', type: 'feature', value: 'enabled') option('systemd-user-unit-dir', type : 'string', description : 'Directory for user systemd units (defaults to /usr/lib/systemd/user)') option('with-module-sets', description : 'Extra modules sets to enable on install (see media-session.conf)', type : 'array', choices : ['alsa', 'jack', 'pulseaudio'], value : ['jack', 'pulseaudio']) 07070100000022000041ED0000000000000000000000026178A88C00000000000000000000000000000000000000000000001700000000media-session-0.4.1/po07070100000023000081A40000000000000000000000016178A88C000000AE000000000000000000000000000000000000001F00000000media-session-0.4.1/po/LINGUASaf as be bg bn_IN ca cs da de_CH de el es fi fr gl gu he hi hr hu id it ja kk kn ko lt ml mr my nl nn oc or pa pl pt_BR pt ru sk sr@latin sr sv ta te tr uk zh_CN zh_TW eo si 07070100000024000081A40000000000000000000000016178A88C00000013000000000000000000000000000000000000002300000000media-session-0.4.1/po/POTFILES.insrc/alsa-monitor.c 07070100000025000081A40000000000000000000000016178A88C00000031000000000000000000000000000000000000002500000000media-session-0.4.1/po/POTFILES.skipsystemd/system/pipewire-media-session.service.in 07070100000026000081A40000000000000000000000016178A88C00000388000000000000000000000000000000000000001D00000000media-session-0.4.1/po/af.po# Afrikaans translation of PipeWire. # This file is distributed under the same license as the PipeWire package. # F Wolff <friedel@translate.org.za>, 2019. msgid "" msgstr "" "Project-Id-Version: master\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2019-01-09 12:17+0200\n" "Last-Translator: F Wolff <friedel@translate.org.za>\n" "Language-Team: translate-discuss-af@lists.sourceforge.net\n" "Language: af\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Virtaal 0.7.1\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Ingeboude oudio" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000027000081A40000000000000000000000016178A88C000003FD000000000000000000000000000000000000001D00000000media-session-0.4.1/po/as.po# translation of pipewire.master-tx.as.po to Assamese # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Amitakhya Phukan <aphukan@fedoraproject.org>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx.as\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:52+0000\n" "Last-Translator: Amitakhya Phukan <aphukan@fedoraproject.org>\n" "Language-Team: Assamese <>\n" "Language: as\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Lokalize 0.2\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "আভ্যন্তৰীণ অ'ডিঅ'" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "মোডেম" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000028000081A40000000000000000000000016178A88C00000418000000000000000000000000000000000000001D00000000media-session-0.4.1/po/be.po# Belarusian tanslation of PipeWire # Copyright (C) 2016 PipeWire's COPYRIGHT HOLDER # This file is distributed under the same license as the PipeWire package. # # Viktar Vaŭčkievič <victorenator@gmail.com>, 2016. # msgid "" msgstr "" "Project-Id-Version: PipeWire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2016-07-19 11:06+0300\n" "Last-Translator: Viktar Vaŭčkievič <victorenator@gmail.com>\n" "Language-Team: Belarusian <>\n" "Language: be\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Lokalize 2.0\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Убудаванае аўдыя" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Мадэм" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000029000081A40000000000000000000000016178A88C0000033F000000000000000000000000000000000000001D00000000media-session-0.4.1/po/bg.po# Valentin Laskov <laskov@festa.bg>, 2016. #zanata msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2020-10-15 21:30+0000\n" "Last-Translator: Emanuil Novachev <em.novachev@gmail.com>\n" "Language-Team: Bulgarian <https://translate.fedoraproject.org/projects/" "pipewire/pipewire/bg/>\n" "Language: bg\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.2.2\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 0707010000002A000081A40000000000000000000000016178A88C00000417000000000000000000000000000000000000002000000000media-session-0.4.1/po/bn_IN.po# translation of pipewire.master-tx.bn_IN.po to Bengali INDIA # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Runa Bhattacharjee <runab@redhat.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx.bn_IN\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:52+0000\n" "Last-Translator: Runa Bhattacharjee <runab@redhat.com>\n" "Language-Team: Bengali INDIA <anubad@lists.ankur.org.in>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: KBabel 1.11.4\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "অভ্যন্তরীণ অডিও" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "মোডেম" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 0707010000002B000081A40000000000000000000000016178A88C0000075D000000000000000000000000000000000000001D00000000media-session-0.4.1/po/ca.po# Catalan translation of pipewire by Softcatalà # Copyright (C) 2008 Free Software Foundation # This file is distributed under the same license as the pipewire package. # # Xavier Conde Rueda <xavi.conde@gmail.com>, 2008. # Agustí Grau <fletxa@gmail.com>, 2009. # Judith Pintó Subirada <judithp@gmail.com> # # This file is translated according to the glossary and style guide of # Softcatalà. If you plan to modify this file, please read first the page # of the Catalan translation team for the Fedora project at: # http://www.softcatala.org/projectes/fedora/ # and contact the previous translator. # # Aquest fitxer s'ha de traduir d'acord amb el recull de termes i la guia # d'estil de Softcatalà. Si voleu modificar aquest fitxer, llegiu si # us plau la pàgina de catalanització del projecte Fedora a: # http://www.softcatala.org/projectes/fedora/ # i contacteu l'anterior traductor/a. # Josep Torné Llavall <josep.torne@gmail.com>, 2009, 2012. # Robert Antoni Buj Gelonch <rbuj@fedoraproject.org>, 2016. #zanata # Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata # Robert Antoni Buj Gelonch <rbuj@fedoraproject.org>, 2017. #zanata # Robert Antoni Buj Gelonch <rbuj@fedoraproject.org>, 2019. #zanata # msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:52+0000\n" "Last-Translator: Josep Torné Llavall <josep.torne@gmail.com>\n" "Language-Team: Catalan <fedora@softcatala.net>\n" "Language: ca\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Àudio intern" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Mòdem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 0707010000002C000081A40000000000000000000000016178A88C00000453000000000000000000000000000000000000001D00000000media-session-0.4.1/po/cs.po# Czech translation of pipewire. # Copyright (C) 2008, 2009 the author(s) of pipewire. # This file is distributed under the same license as the pipewire package. # Petr Kovar <pknbe@volny.cz>, 2008, 2009, 2012. # Daniel Rusek <mail@asciiwolf.com>, 2018. # Marek Černocký <marek@manet.cz>, 2018. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-10-12 14:18+0200\n" "Last-Translator: Daniel Rusek <mail@asciiwolf.com>\n" "Language-Team: čeština <gnome-cs-list@gnome.org>\n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "X-Generator: Poedit 3.0\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Vnitřní zvukový systém" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Neznámé zařízení" 0707010000002D000081A40000000000000000000000016178A88C00000363000000000000000000000000000000000000001D00000000media-session-0.4.1/po/da.po# Danish translation for PipeWire. # Copyright (C) 2019 PipeWire's COPYRIGHT HOLDER # This file is distributed under the same license as the PipeWire package. # scootergrisen, 2019, 2021. msgid "" msgstr "" "Project-Id-Version: PipeWire master\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-04-19 20:26+0200\n" "Last-Translator: scootergrisen\n" "Language-Team: Danish\n" "Language: da\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Indbygget lyd" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Ukendt enhed" 0707010000002E000081A40000000000000000000000016178A88C000004B1000000000000000000000000000000000000001D00000000media-session-0.4.1/po/de.po# translation of pipewire.master-tx.de.po to # German translation of pipewire # Copyright (C) 2008 pipewire # This file is distributed under the same license as the pipewire package. # # # Fabian Affolter <fab@fedoraproject.org>, 2008-2009. # Micha Pietsch <barney@fedoraproject.org>, 2008, 2009. # Hedda Peters <hpeters@redhat.com>, 2009, 2012. # Mario Blättermann <mario.blaettermann@gmail.com>, 2016. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx.de\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-01-09 11:36+0000\n" "Last-Translator: Tobias Weise <tobias.weise@web.de>\n" "Language-Team: German <https://translate.fedoraproject.org/projects/pipewire/" "pipewire/de/>\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Internes Audio" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 0707010000002F000081A40000000000000000000000016178A88C000003EC000000000000000000000000000000000000002000000000media-session-0.4.1/po/de_CH.po# German translation of pipewire # Copyright (C) 2008 pipewire # This file is distributed under the same license as the pipewire package. # # Micha Pietsch <barney@fedoraproject.org>, 2008, 2009. # Fabian Affolter <fab@fedoraproject.org>, 2008-2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:53+0000\n" "Last-Translator: Fabian Affolter <fab@fedoraproject.org>\n" "Language-Team: German <fedora-trans-de@redhat.com>\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Poedit-Language: German\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Internes Audio" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000030000081A40000000000000000000000016178A88C00000443000000000000000000000000000000000000001D00000000media-session-0.4.1/po/el.po# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Dimitris Glezos <dimitris@glezos.com>, 2008. # Thalia Papoutsaki <saliyath@gmail.com>, 2009, 2012. # Dimitris Spingos (Δημήτρης Σπίγγος) <dmtrs32@gmail.com>, 2013, 2014. msgid "" msgstr "" "Project-Id-Version: el\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2014-05-08 09:53+0300\n" "Last-Translator: Dimitris Spingos (Δημήτρης Σπίγγος) <dmtrs32@gmail.com>\n" "Language-Team: team@lists.gnome.gr\n" "Language: el\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Virtaal 0.7.0\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Εσωτερικός ήχος" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Μόντεμ" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000031000081A40000000000000000000000016178A88C000003D9000000000000000000000000000000000000001D00000000media-session-0.4.1/po/eo.po# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the pipewire package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-02-05 01:40+0000\n" "Last-Translator: Carmen Bianca Bakker <carmen@carmenbianca.eu>\n" "Language-Team: Esperanto <https://translate.fedoraproject.org/projects/" "pipewire/pipewire/eo/>\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4.2\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Integrita sono" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000032000081A40000000000000000000000016178A88C0000054B000000000000000000000000000000000000001D00000000media-session-0.4.1/po/es.po# Fedora Spanish translation of PipeWire. # This file is distributed under the same license as the PipeWire Package. # # Domingo Becker <domingobecker@gmail.com>, 2009. # Héctor Daniel Cabrera <h.daniel.cabrera@gmail.com>, 2009. # Fernando Gonzalez Blanco <fgonz@fedoraproject.org>, 2009, 2012. # Alberto Castillo <acg@albertocg.com>, 2016. #zanata # Gladys Guerrero Lozano <gguerrer@redhat.com>, 2016. #zanata # Máximo Castañeda Riloba <mcrcctm@gmail.com>, 2016. #zanata # Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2020-10-01 15:30+0000\n" "Last-Translator: Emilio Herrera <ehespinosa57@gmail.com>\n" "Language-Team: Spanish <https://translate.fedoraproject.org/projects/" "pipewire/pipewire/es/>\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.2.2\n" "X-Poedit-Language: Spanish\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Audio Interno" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Módem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000033000081A40000000000000000000000016178A88C0000042A000000000000000000000000000000000000001D00000000media-session-0.4.1/po/fi.po# pipewire translation to Finnish (fi). # Copyright (C) 2008 Timo Jyrinki # This file is distributed under the same license as the pipewire package. # Timo Jyrinki <timo.jyrinki@iki.fi>, 2008. # Ville-Pekka Vainio <vpivaini@cs.helsinki.fi>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: git trunk\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-04-18 16:38+0300\n" "Last-Translator: Ricky Tigg <ricky.tigg@gmail.com>\n" "Language-Team: Finnish <https://translate.fedoraproject.org/projects/" "pipewire/pipewire/fi/>\n" "Language: fi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.5.1\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Sisäinen äänentoisto" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modeemi" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Tuntematon laite" 07070100000034000081A40000000000000000000000016178A88C00000570000000000000000000000000000000000000001D00000000media-session-0.4.1/po/fr.po# French translation of pipewire. # Copyright (C) 2006-2008 Lennart Poettering # This file is distributed under the same license as the pipewire package. # # # Robert-André Mauchin <zebob.m@pengzone.org>, 2008. # Michaël Ughetto <telimektar esraonline com>, 2008. # Pablo Martin-Gomez <pablo.martin-gomez@laposte.net>, 2008. # Corentin Perard <corentin.perard@gmail.com>, 2009. # Thomas Canniot <mrtom@fedoraproject.org>, 2009, 2012. # Sam Friedmann <sfriedma@redhat.com>, 2016. #zanata # Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata # Edouard Duliege <edouard.duliege@gmail.com>, 2017. #zanata msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2020-12-13 17:35+0000\n" "Last-Translator: Julien Humbert <julroy67@gmail.com>\n" "Language-Team: French <https://translate.fedoraproject.org/projects/pipewire/" "pipewire/fr/>\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.3.2\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Audio interne" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000035000081A40000000000000000000000016178A88C0000042B000000000000000000000000000000000000001D00000000media-session-0.4.1/po/gl.po# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Translators: # bassball93 <bassball93@gmail.com>, 2011. # mbouzada <mbouzada@gmail.com>, 2011. # Fran Dieguez <frandieguez@gnome.org>, 2012, 2019. # Marcos Lans <marcoslansgarza@gmail.com>, 2018. msgid "" msgstr "" "Project-Id-Version: PipeWire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2019-02-20 01:36+0200\n" "Last-Translator: Fran Dieguez <frandieguez@gnome.org>\n" "Language-Team: Galician\n" "Language: gl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Virtaal 0.7.1\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Audio interno" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Módem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000036000081A40000000000000000000000016178A88C000003C2000000000000000000000000000000000000001D00000000media-session-0.4.1/po/gu.po# translation of PipeWire.po to Gujarati # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Sweta Kothari <swkothar@redhat.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: PipeWire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:53+0000\n" "Last-Translator: Sweta Kothari <swkothar@redhat.com>\n" "Language-Team: Gujarati\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: KBabel 1.11.4\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "આંતરિક ઓડિયો" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "મોડેમ" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000037000081A40000000000000000000000016178A88C0000037C000000000000000000000000000000000000001D00000000media-session-0.4.1/po/he.po# # Elad <el.il@doom.co.il>, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-03-02 14:40+0000\n" "Last-Translator: Yaron Shahrabani <sh.yaron@gmail.com>\n" "Language-Team: Hebrew <https://translate.fedoraproject.org/projects/pipewire/" "pipewire/he/>\n" "Language: he\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Weblate 4.4.2\n" "X-Poedit-Language: Hebrew\n" "X-Poedit-Country: Israel\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "צליל פנימי" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "מודם" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000038000081A40000000000000000000000016178A88C00000404000000000000000000000000000000000000001D00000000media-session-0.4.1/po/hi.po# translation of pipewire.master-tx.po to Hindi # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Rajesh Ranjan <rajesh672@gmail.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:54+0000\n" "Last-Translator: Rajesh Ranjan <rajesh672@gmail.com>\n" "Language-Team: Hindi <hindi.sf.net>\n" "Language: hi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: KBabel 1.11.4\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "आंतरिक ऑडियो" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "मॉडेम" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000039000081A40000000000000000000000016178A88C0000046E000000000000000000000000000000000000001D00000000media-session-0.4.1/po/hr.po# Croatian translation for pipewire # Copyright (c) 2010 Rosetta Contributors and Canonical Ltd 2010 # This file is distributed under the same license as the pipewire package. # gogo <trebelnik2@gmail.com>, 2017. # msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-10-23 19:13+0200\n" "Last-Translator: gogo <trebelnik2@gmail.com>\n" "Language-Team: Croatian <https://translate.fedoraproject.org/projects/" "pipewire/pipewire/hr/>\n" "Language: hr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Poedit 2.3\n" "X-Launchpad-Export-Date: 2017-04-20 21:04+0000\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Ugrađeni zvuk" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Nepoznat uređaj" 0707010000003A000081A40000000000000000000000016178A88C000004B7000000000000000000000000000000000000001D00000000media-session-0.4.1/po/hu.po# Hungarian translation of PipeWire # Copyright (C) 2012, 2016. Free Software Foundation, Inc. # This file is distributed under the same license as the PipeWire package. # # KAMI <kami911@gmail.com>, 2012. # Gabor Kelemen <kelemeng at ubuntu dot com>, 2016. # Balázs Úr <urbalazs at gmail dot com>, 2016. msgid "" msgstr "" "Project-Id-Version: PipeWire master\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2020-07-21 15:29+0000\n" "Last-Translator: Balázs Meskó <meskobalazs@mailbox.org>\n" "Language-Team: Hungarian <https://translate.fedoraproject.org/projects/" "pipewire/pipewire/hu/>\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.1.1\n" "X-Poedit-Language: Hungarian\n" "X-Poedit-Country: HUNGARY\n" "X-Poedit-SourceCharset: utf-8\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Belső hangforrás" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 0707010000003B000081A40000000000000000000000016178A88C000003DC000000000000000000000000000000000000001D00000000media-session-0.4.1/po/id.po# Indonesian translation of pipewire # Copyright (C) 2011 THE pipewire'S COPYRIGHT HOLDER # This file is distributed under the same license as the pipewire package. # # Translators: # Andika Triwidada <andika@gmail.com>, 2011, 2012, 2018. msgid "" msgstr "" "Project-Id-Version: PipeWire master\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-08-11 10:50+0700\n" "Last-Translator: Andika Triwidada <andika@gmail.com>\n" "Language-Team: Indonesia <id@li.org>\n" "Language: id\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n>1;\n" "X-Generator: Poedit 2.2.1\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Audio Bawaan" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Perangkat tak dikenal" 0707010000003C000081A40000000000000000000000016178A88C000004A8000000000000000000000000000000000000001D00000000media-session-0.4.1/po/it.po# Italian translation for PipeWire. # Copyright (C) 2008, 2009, 2012, 2015, 2019 The Free Software Foundation, Inc # This file is distributed under the same license as the pipewire package. # # Luca Ferretti <elle.uca@libero.it>, 2008, 2009. # mario_santagiuliana <mario at marionline.it>, 2009. # Milo Casagrande <milo@ubuntu.com>, 2009, 2012, 2015, 2019. # Albano Battistella <albano_battistella@hotmail.com>,2021 msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-05-09 13:29+0100\n" "Last-Translator: Albano Battistella <albano_battistella@hotmail.com>\n" "Language-Team: Italian <>pipewire/pipewire/it/>\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.2.2\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Audio interno" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Dispositivo sconosciuto" 0707010000003D000081A40000000000000000000000016178A88C00000497000000000000000000000000000000000000001D00000000media-session-0.4.1/po/ja.po# translation of ja.po to Japanese # PipeWire # Copyright (C) 2009. # This file is distributed under the same license as the PACKAGE package. # # Hyu_gabaru Ryu_ichi <hyu_gabaru@yahoo.co.jp>, 2009. # Kiyoto Hashida <khashida@redhat.com>, 2009, 2012. # Kenzo Moriguchi <kmoriguc@redhat.com>, 2016. #zanata # Ooyama Yosiyuki <qqke6wd9k@apricot.ocn.ne.jp>, 2016. #zanata # Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2016-03-15 08:25+0000\n" "Last-Translator: Kenzo Moriguchi <kmoriguc@redhat.com>\n" "Language-Team: Japanese <jp@li.org>\n" "Language: ja\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Zanata 4.6.2\n" "Plural-Forms: Plural-Forms: nplurals=1; plural=0;\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "内部オーディオ" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "モデム" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 0707010000003E000081A40000000000000000000000016178A88C000003B2000000000000000000000000000000000000001D00000000media-session-0.4.1/po/kk.po# Kazakh translation of pipewire. # Copyright (C) 2020 The pipewire authors. # This file is distributed under the same license as the pipewire package. # Baurzhan Muftakhidinov <baurthefirst@gmail.com>, 2020. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2020-06-30 08:04+0500\n" "Last-Translator: Baurzhan Muftakhidinov <baurthefirst@gmail.com>\n" "Language-Team: \n" "Language: kk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.3.1\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Құрамындағы аудио" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Модем" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 0707010000003F000081A40000000000000000000000016178A88C000003F9000000000000000000000000000000000000001D00000000media-session-0.4.1/po/kn.po# translation of pipewire.master-tx.kn.po to Kannada # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Shankar Prasad <svenkate@redhat.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx.kn\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:54+0000\n" "Last-Translator: Shankar Prasad <svenkate@redhat.com>\n" "Language-Team: Kannada <kde-l10n-kn@kde.org>\n" "Language: kn\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Lokalize 1.0\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "ಆಂತರಿಕ ಆಡಿಯೊ" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "ಮಾಡೆಮ್" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000040000081A40000000000000000000000016178A88C00000352000000000000000000000000000000000000001D00000000media-session-0.4.1/po/ko.po# eukim <eukim@redhat.com>, 2013. #zanata # KimJeongYeon <jeongyeon.kim@samsung.com>, 2017. # Sangchul Lee <sc11.lee@samsung.com>, 2018. msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2018-06-21 15:10+0900\n" "Last-Translator: Sangchul Lee <sc11.lee@samsung.com>\n" "Language-Team: Korean\n" "Language: ko\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.8.7.1\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "내장 오디오 " #: src/alsa-monitor.c:666 msgid "Modem" msgstr "모뎀 " #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000041000081A40000000000000000000000016178A88C0000038D000000000000000000000000000000000000001D00000000media-session-0.4.1/po/lt.po# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Moo, 2017-2019 # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2019-09-01 16:15+0300\n" "Last-Translator: Moo\n" "Language-Team: \n" "Language: lt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.2.1\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n" "%100<10 || n%100>=20) ? 1 : 2);\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Įtaisytas garsas" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modemas" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000042000081A40000000000000000000000016178A88C00000338000000000000000000000000000000000000002900000000media-session-0.4.1/po/media-session.pot# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the media-session package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: media-session\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/media-session/" "issues/new\n" "POT-Creation-Date: 2021-10-20 09:59+1000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000043000081A40000000000000000000000016178A88C00000133000000000000000000000000000000000000002300000000media-session-0.4.1/po/meson.buildi18n = import('i18n') i18n.gettext( meson.project_name(), preset: 'glib', # Page width is set to 90 characters in order to avoid bad wrapping of the # bug reporting address. args: ['--msgid-bugs-address=https://gitlab.freedesktop.org/pipewire/media-session/issues/new', '--width=90'], ) 07070100000044000081A40000000000000000000000016178A88C000002CD000000000000000000000000000000000000001D00000000media-session-0.4.1/po/ml.po# # <>, YEAR, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx.ml\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:41+0000\n" "Last-Translator: \n" "Language-Team: <en@li.org>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: KBabel 1.11.4\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "ഇന്റേര്ണല് ഓഡിയോ" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "മോഡം" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000045000081A40000000000000000000000016178A88C000003F6000000000000000000000000000000000000001D00000000media-session-0.4.1/po/mr.po# translation of pipewire.master-tx.po to Marathi # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Sandeep Shedmake <sshedmak@redhat.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:54+0000\n" "Last-Translator: Sandeep Shedmake <sshedmak@redhat.com>\n" "Language-Team: Marathi <fedora-trans-mr@redhat.com>\n" "Language: mr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: KBabel 1.11.4\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "आंतरीक ऑडिओ" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "मोडेम" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000046000081A40000000000000000000000016178A88C00000419000000000000000000000000000000000000001D00000000media-session-0.4.1/po/my.po# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # msgid "" msgstr "" "Project-Id-Version: pipewire-master\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-08-26 21:52+0630\n" "Last-Translator: zayar lwin <lw1nzayar@yandex.com>\n" "Language-Team: lw1nzayar@yandex.com\n" "Language: my\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Poedit 2.4.2\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "နဂိုတည်းကထည့်သွင်းထားသည်ံ့ အသံကရိယာ" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "ဆ.သ.ရ-စက်" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "မသိသောစက်" 07070100000047000081A40000000000000000000000016178A88C00000453000000000000000000000000000000000000001D00000000media-session-0.4.1/po/nl.po# Dutch translation of pipewire.master-tx. # Copyright (C) 2009 THE pipewire.master-tx'S COPYRIGHT HOLDER # This file is distributed under the same license as the pipewire.master-tx package. # Geert Warrink <geert.warrink@onsnet.nu>, 2009. # Reinout van Schouwen <reinout@gmail.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2020-07-11 20:27+0000\n" "Last-Translator: Geert Warrink <geert.warrink@onsnet.nu>\n" "Language-Team: Dutch <https://translate.fedoraproject.org/projects/pipewire/" "pipewire/nl/>\n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.1.1\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Intern geluid" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Onbekend apparaat" 07070100000048000081A40000000000000000000000016178A88C000003FF000000000000000000000000000000000000001D00000000media-session-0.4.1/po/nn.po# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Karl Ove Hufthammer <karl@huftis.org>, 2017. # Nicolai Syvertsen <saivert@saivert.com> 2021. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-02-07 15:40+0000\n" "Last-Translator: Karl Ove Hufthammer <karl@huftis.org>\n" "Language-Team: Norwegian Nynorsk <https://translate.fedoraproject.org/" "projects/pipewire/pipewire/nn/>\n" "Language: nn\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4.2\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Innebygd lyd" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Ukjend einhet" 07070100000049000081A40000000000000000000000016178A88C00000539000000000000000000000000000000000000001D00000000media-session-0.4.1/po/oc.po# French translation of pipewire. # Copyright (C) 2006-2008 Lennart Poettering # This file is distributed under the same license as the pipewire package. # Robert-André Mauchin <zebob.m@pengzone.org>, 2008. # Michaël Ughetto <telimektar esraonline com>, 2008. # Pablo Martin-Gomez <pablo.martin-gomez@laposte.net>, 2008. # Corentin Perard <corentin.perard@gmail.com>, 2009. # Thomas Canniot <mrtom@fedoraproject.org>, 2009, 2012. # Cédric Valmary (Tot en Òc) <cvalmary@yahoo.fr>, 2015. # Cédric Valmary (totenoc.eu) <cvalmary@yahoo.fr>, 2016. msgid "" msgstr "" "Project-Id-Version: pipewire trunk\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2016-10-12 22:20+0200\n" "Last-Translator: Cédric Valmary (totenoc.eu) <cvalmary@yahoo.fr>\n" "Language-Team: Tot En Òc\n" "Language: oc\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Virtaal 0.7.1\n" "X-Launchpad-Export-Date: 2016-10-12 20:12+0000\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Àudio integrat" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modèm" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 0707010000004A000081A40000000000000000000000016178A88C0000044E000000000000000000000000000000000000001D00000000media-session-0.4.1/po/or.po# translation of pipewire.master-tx.or.po to Oriya # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Manoj Kumar Giri <mgiri@redhat.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx.or\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:55+0000\n" "Last-Translator: Manoj Kumar Giri <mgiri@redhat.com>\n" "Language-Team: Oriya <oriya-it@googlegroups.com>\n" "Language: or\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: KBabel 1.11.4\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "ଆଭ୍ୟନ୍ତରୀଣ ଧ୍ୱନି" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "ମଡେମ" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 0707010000004B000081A40000000000000000000000016178A88C00000452000000000000000000000000000000000000001D00000000media-session-0.4.1/po/pa.po# translation of pipewire.master-tx.pa.po to Punjabi # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # # Amanpreet Singh Alam <aalam@users.sf.net>, 2008. # A S Alam <aalam@users.sf.net>, 2009. # Jaswinder Singh <jsingh@redhat.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx.pa\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:55+0000\n" "Last-Translator: Jaswinder Singh <jsingh@redhat.com>\n" "Language-Team: Punjabi/Panjabi <kde-i18n-doc@kde.org>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Lokalize 1.0\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "ਅੰਦਰੂਨੀ ਆਡੀਓ" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "ਮਾਡਮ" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 0707010000004C000081A40000000000000000000000016178A88C00003708000000000000000000000000000000000000002400000000media-session-0.4.1/po/pipewire.pot# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the pipewire package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/issues/new\n" "POT-Creation-Date: 2021-04-18 16:54+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" #: src/daemon/pipewire.c:43 #, c-format msgid "" "%s [options]\n" " -h, --help Show this help\n" " --version Show version\n" " -c, --config Load config (Default %s)\n" msgstr "" #: src/daemon/pipewire.desktop.in:4 msgid "PipeWire Media System" msgstr "" #: src/daemon/pipewire.desktop.in:5 msgid "Start the PipeWire Media System" msgstr "" #: src/examples/media-session/alsa-monitor.c:526 spa/plugins/alsa/acp/compat.c:187 msgid "Built-in Audio" msgstr "" #: src/examples/media-session/alsa-monitor.c:530 spa/plugins/alsa/acp/compat.c:192 msgid "Modem" msgstr "" #: src/examples/media-session/alsa-monitor.c:539 msgid "Unknown device" msgstr "" #: src/tools/pw-cat.c:991 #, c-format msgid "" "%s [options] <file>\n" " -h, --help Show this help\n" " --version Show version\n" " -v, --verbose Enable verbose operations\n" "\n" msgstr "" #: src/tools/pw-cat.c:998 #, c-format msgid "" " -R, --remote Remote daemon name\n" " --media-type Set media type (default %s)\n" " --media-category Set media category (default %s)\n" " --media-role Set media role (default %s)\n" " --target Set node target (default %s)\n" " 0 means don't link\n" " --latency Set node latency (default %s)\n" " Xunit (unit = s, ms, us, ns)\n" " or direct samples (256)\n" " the rate is the one of the source file\n" " --list-targets List available targets for --target\n" "\n" msgstr "" #: src/tools/pw-cat.c:1016 #, c-format msgid "" " --rate Sample rate (req. for rec) (default %u)\n" " --channels Number of channels (req. for rec) (default %u)\n" " --channel-map Channel map\n" " one of: \"stereo\", \"surround-51\",... or\n" " comma separated list of channel names: eg. " "\"FL,FR\"\n" " --format Sample format %s (req. for rec) (default %s)\n" " --volume Stream volume 0-1.0 (default %.3f)\n" " -q --quality Resampler quality (0 - 15) (default %d)\n" "\n" msgstr "" #: src/tools/pw-cat.c:1033 msgid "" " -p, --playback Playback mode\n" " -r, --record Recording mode\n" " -m, --midi Midi mode\n" "\n" msgstr "" #: src/tools/pw-cli.c:2932 #, c-format msgid "" "%s [options] [command]\n" " -h, --help Show this help\n" " --version Show version\n" " -d, --daemon Start as daemon (Default false)\n" " -r, --remote Remote daemon name\n" "\n" msgstr "" #: spa/plugins/alsa/acp/acp.c:290 msgid "Pro Audio" msgstr "" #: spa/plugins/alsa/acp/acp.c:411 spa/plugins/alsa/acp/alsa-mixer.c:4704 #: spa/plugins/bluez5/bluez5-device.c:1000 msgid "Off" msgstr "" #: spa/plugins/alsa/acp/channelmap.h:466 msgid "(invalid)" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2709 msgid "Input" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2710 msgid "Docking Station Input" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2711 msgid "Docking Station Microphone" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2712 msgid "Docking Station Line In" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2713 spa/plugins/alsa/acp/alsa-mixer.c:2804 msgid "Line In" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2714 spa/plugins/alsa/acp/alsa-mixer.c:2798 #: spa/plugins/bluez5/bluez5-device.c:1145 msgid "Microphone" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2715 spa/plugins/alsa/acp/alsa-mixer.c:2799 msgid "Front Microphone" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2716 spa/plugins/alsa/acp/alsa-mixer.c:2800 msgid "Rear Microphone" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2717 msgid "External Microphone" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2718 spa/plugins/alsa/acp/alsa-mixer.c:2802 msgid "Internal Microphone" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2719 spa/plugins/alsa/acp/alsa-mixer.c:2805 msgid "Radio" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2720 spa/plugins/alsa/acp/alsa-mixer.c:2806 msgid "Video" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2721 msgid "Automatic Gain Control" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2722 msgid "No Automatic Gain Control" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2723 msgid "Boost" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2724 msgid "No Boost" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2725 msgid "Amplifier" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2726 msgid "No Amplifier" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2727 msgid "Bass Boost" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2728 msgid "No Bass Boost" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2729 spa/plugins/bluez5/bluez5-device.c:1150 msgid "Speaker" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2730 spa/plugins/alsa/acp/alsa-mixer.c:2808 msgid "Headphones" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2797 msgid "Analog Input" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2801 msgid "Dock Microphone" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2803 msgid "Headset Microphone" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2807 msgid "Analog Output" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2809 msgid "Headphones 2" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2810 msgid "Headphones Mono Output" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2811 msgid "Line Out" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2812 msgid "Analog Mono Output" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2813 msgid "Speakers" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2814 msgid "HDMI / DisplayPort" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2815 msgid "Digital Output (S/PDIF)" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2816 msgid "Digital Input (S/PDIF)" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2817 msgid "Multichannel Input" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2818 msgid "Multichannel Output" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2819 msgid "Game Output" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2820 spa/plugins/alsa/acp/alsa-mixer.c:2821 msgid "Chat Output" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2822 msgid "Chat Input" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:2823 msgid "Virtual Surround 7.1" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4527 msgid "Analog Mono" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4528 msgid "Analog Mono (Left)" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4529 msgid "Analog Mono (Right)" msgstr "" #. Note: Not translated to "Analog Stereo Input", because the source #. * name gets "Input" appended to it automatically, so adding "Input" #. * here would lead to the source name to become "Analog Stereo Input #. * Input". The same logic applies to analog-stereo-output, #. * multichannel-input and multichannel-output. #: spa/plugins/alsa/acp/alsa-mixer.c:4530 spa/plugins/alsa/acp/alsa-mixer.c:4538 #: spa/plugins/alsa/acp/alsa-mixer.c:4539 msgid "Analog Stereo" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4531 msgid "Mono" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4532 msgid "Stereo" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4540 spa/plugins/alsa/acp/alsa-mixer.c:4698 #: spa/plugins/bluez5/bluez5-device.c:1135 msgid "Headset" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4541 spa/plugins/alsa/acp/alsa-mixer.c:4699 msgid "Speakerphone" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4542 spa/plugins/alsa/acp/alsa-mixer.c:4543 msgid "Multichannel" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4544 msgid "Analog Surround 2.1" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4545 msgid "Analog Surround 3.0" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4546 msgid "Analog Surround 3.1" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4547 msgid "Analog Surround 4.0" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4548 msgid "Analog Surround 4.1" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4549 msgid "Analog Surround 5.0" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4550 msgid "Analog Surround 5.1" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4551 msgid "Analog Surround 6.0" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4552 msgid "Analog Surround 6.1" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4553 msgid "Analog Surround 7.0" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4554 msgid "Analog Surround 7.1" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4555 msgid "Digital Stereo (IEC958)" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4556 msgid "Digital Surround 4.0 (IEC958/AC3)" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4557 msgid "Digital Surround 5.1 (IEC958/AC3)" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4558 msgid "Digital Surround 5.1 (IEC958/DTS)" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4559 msgid "Digital Stereo (HDMI)" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4560 msgid "Digital Surround 5.1 (HDMI)" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4561 msgid "Chat" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4562 msgid "Game" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4696 msgid "Analog Mono Duplex" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4697 msgid "Analog Stereo Duplex" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4700 msgid "Digital Stereo Duplex (IEC958)" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4701 msgid "Multichannel Duplex" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4702 msgid "Stereo Duplex" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4703 msgid "Mono Chat + 7.1 Surround" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4806 #, c-format msgid "%s Output" msgstr "" #: spa/plugins/alsa/acp/alsa-mixer.c:4813 #, c-format msgid "%s Input" msgstr "" #: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269 #, c-format msgid "" "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA " "developers." msgid_plural "" "snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA " "developers." msgstr[0] "" msgstr[1] "" #: spa/plugins/alsa/acp/alsa-util.c:1241 #, c-format msgid "" "snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA " "developers." msgid_plural "" "snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA " "developers." msgstr[0] "" msgstr[1] "" #: spa/plugins/alsa/acp/alsa-util.c:1288 #, c-format msgid "" "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail %lu.\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA " "developers." msgstr "" #: spa/plugins/alsa/acp/alsa-util.c:1331 #, c-format msgid "" "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte (%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA " "developers." msgid_plural "" "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes (%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA " "developers." msgstr[0] "" msgstr[1] "" #: spa/plugins/bluez5/bluez5-device.c:1010 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1033 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1035 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1041 msgid "High Fidelity Playback (A2DP Sink)" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1043 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1070 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1074 msgid "Headset Head Unit (HSP/HFP)" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1140 msgid "Handsfree" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1155 msgid "Headphone" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1160 msgid "Portable" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1165 msgid "Car" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1170 msgid "HiFi" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1175 msgid "Phone" msgstr "" #: spa/plugins/bluez5/bluez5-device.c:1181 msgid "Bluetooth" msgstr "" 0707010000004D000081A40000000000000000000000016178A88C000003F0000000000000000000000000000000000000001D00000000media-session-0.4.1/po/pl.po# Polish translation for pipewire. # Copyright © 2008-2021 the pipewire authors. # This file is distributed under the same license as the pipewire package. # Piotr Drąg <piotrdrag@gmail.com>, 2008, 2012-2021. # msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-10-09 15:35+0200\n" "Last-Translator: Piotr Drąg <piotrdrag@gmail.com>\n" "Language-Team: Polish <community-poland@mozilla.org>\n" "Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2;\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Wbudowany dźwięk" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Nieznane urządzenie" 0707010000004E000081A40000000000000000000000016178A88C00000417000000000000000000000000000000000000001D00000000media-session-0.4.1/po/pt.po# # Rui Gouveia <rui.gouveia@globaltek.pt>, 2012. # Rui Gouveia <rui.gouveia@globaltek.pt>, 2012, 2015. # Pedro Albuquerque <palbuquerque73@openmailbox.com>, 2015. # Juliano de Souza Camargo <julianosc@pm.me>, 2020. # Hugo Carvalho <hugokarvalho@hotmail.com>, 2021. # msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-08-02 15:28+0100\n" "Last-Translator: Hugo Carvalho <hugokarvalho@hotmail.com>\n" "Language-Team: Portuguese <https://l10n.gnome.org/teams/pt/>\n" "Language: pt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.0\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Áudio interno" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Dispositivo desconhecido" 0707010000004F000081A40000000000000000000000016178A88C0000046D000000000000000000000000000000000000002000000000media-session-0.4.1/po/pt_BR.po# Brazilian Portuguese translation for pipewire # Copyright (C) 2021 Rafael Fontenelle <rafaelff@gnome.org> # This file is distributed under the same license as the pipewire package. # Fabian Affolter <fab@fedoraproject.org>, 2008. # Igor Pires Soares <igor@projetofedora.org>, 2009, 2012. # Rafael Fontenelle <rafaelff@gnome.org>, 2013-2021. # msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-08-01 17:02-0300\n" "Last-Translator: Rafael Fontenelle <rafaelff@gnome.org>\n" "Language-Team: Brazilian Portuguese <gnome-pt_br-list@gnome.org>\n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" "X-Generator: Gtranslator 40.0\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Áudio interno" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Dispositivo desconhecido" 07070100000050000081A40000000000000000000000016178A88C0000047E000000000000000000000000000000000000001D00000000media-session-0.4.1/po/ru.po# Russian translation of pipewire. # Copyright (C) 2010 pipewire # This file is distributed under the same license as the pipewire package. # # Leonid Kurbatov <llenchikk@rambler.ru>, 2010, 2012. # Alexander Potashev <aspotashev@gmail.com>, 2014, 2019. # Victor Ryzhykh <victorr2007@yandex.ru>, 2021. msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-06-30 13:26+0300\n" "Last-Translator: Alexey Rubtsov <rushills@gmail.com>\n" "Language-Team: ru\n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Poedit 3.0\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Встроенное аудио" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Модем" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Неизвестное устройство" 07070100000051000081A40000000000000000000000016178A88C00000348000000000000000000000000000000000000001D00000000media-session-0.4.1/po/si.po# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the pipewire package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: si\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000052000081A40000000000000000000000016178A88C00000407000000000000000000000000000000000000001D00000000media-session-0.4.1/po/sk.po# Slovak translation for PipeWire. # Copyright (C) 2014 PipeWire's COPYRIGHT HOLDER # This file is distributed under the same license as the PipeWire package. # Dušan Kazik <prescott66@gmail.com>, 2014, 2015. # msgid "" msgstr "" "Project-Id-Version: PipeWire master\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2020-11-25 08:35+0000\n" "Last-Translator: Dusan Kazik <prescott66@gmail.com>\n" "Language-Team: Slovak <https://translate.fedoraproject.org/projects/pipewire/" "pipewire/sk/>\n" "Language: sk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 1 : (n>=2 && n<=4) ? 2 : 0;\n" "X-Generator: Weblate 4.3.2\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Vstavaný zvuk" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000053000081A40000000000000000000000016178A88C0000044A000000000000000000000000000000000000001D00000000media-session-0.4.1/po/sr.po# Serbian translations for pipewire # Copyright (C) 2006 Lennart Poettering # This file is distributed under the same license as the pipewire package. # Igor Miletic (Игор Милетић) <grejigl-gnomeprevod@yahoo.ca>, 2009. # Miloš Komarčević <kmilos@gmail.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:55+0000\n" "Last-Translator: Miloš Komarčević <kmilos@gmail.com>\n" "Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Унутрашњи звук" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Модем" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000054000081A40000000000000000000000016178A88C00000437000000000000000000000000000000000000002300000000media-session-0.4.1/po/sr@latin.po# Serbian(Latin) translations for pipewire # Copyright (C) 2006 Lennart Poettering # This file is distributed under the same license as the pipewire package. # Igor Miletic (Igor Miletić) <grejigl-gnomeprevod@yahoo.ca>, 2009. # Miloš Komarčević <kmilos@gmail.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:55+0000\n" "Last-Translator: Miloš Komarčević <kmilos@gmail.com>\n" "Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Unutrašnji zvuk" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000055000081A40000000000000000000000016178A88C00000561000000000000000000000000000000000000001D00000000media-session-0.4.1/po/sv.po# Swedish translation for pipewire. # Copyright © 2008-2021 Free Software Foundation, Inc. # This file is distributed under the same license as the pipewire package. # Daniel Nylander <po@danielnylander.se>, 2008, 2012. # Josef Andersson <josef.andersson@fripost.org>, 2014, 2017. # Anders Jonsson <anders.jonsson@norsjovallen.se>, 2021. # # Termer: # input/output: ingång/utgång (det handlar om ljud) # latency: latens # delay: fördröjning # boost: öka # gain: förstärkning # channel map: kanalmappning # passthrough: genomströmning # och en hel del termer som inte översätts inom ljuddomänen, ex. surround. msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-09-21 21:51+0200\n" "Last-Translator: Anders Jonsson <anders.jonsson@norsjovallen.se>\n" "Language-Team: Swedish <tp-sv@listor.tp-sv.se>\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Poedit 3.0\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Inbyggt ljud" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Okänd enhet" 07070100000056000081A40000000000000000000000016178A88C0000045B000000000000000000000000000000000000001D00000000media-session-0.4.1/po/ta.po# translation of pipewire.master-tx.ta.po to Tamil # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # I. Felix <ifelix@redhat.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx.ta\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:56+0000\n" "Last-Translator: I. Felix <ifelix@redhat.com>\n" "Language-Team: Tamil <fedora-trans-ta@redhat.com>\n" "Language: ta\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: KBabel 1.11.4\n" "Plural-Forms: nplurals=2; plural=(n!=1);\\n\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "உட்புற ஆடியோ" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "மாதிரி" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000057000081A40000000000000000000000016178A88C00000416000000000000000000000000000000000000001D00000000media-session-0.4.1/po/te.po# translation of pipewire.master-tx.te.po to Telugu # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Krishna Babu K <kkrothap@redhat.com>, 2009, 2012. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx.te\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2012-01-30 09:56+0000\n" "Last-Translator: Krishna Babu K <kkrothap@redhat.com>\n" "Language-Team: Telugu <en@li.org>\n" "Language: te\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: KBabel 1.11.4\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "అంతర్గత ఆడియో" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "మోడెమ్" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 07070100000058000081A40000000000000000000000016178A88C0000048F000000000000000000000000000000000000001D00000000media-session-0.4.1/po/tr.po# Turkish translation for PipeWire. # Copyright (C) 2014 PipeWire's COPYRIGHT HOLDER # This file is distributed under the same license as the PipeWire package. # Necdet Yücel <necdetyucel@gmail.com>, 2014. # Kaan Özdinçer <kaanozdincer@gmail.com>, 2014. # Muhammet Kara <muhammetk@gmail.com>, 2015, 2016, 2017. # Oğuz Ersen <oguzersen@protonmail.com>, 2021. # msgid "" msgstr "" "Project-Id-Version: PipeWire master\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-08-15 19:51+0300\n" "Last-Translator: Oğuz Ersen <oguzersen@protonmail.com>\n" "Language-Team: Turkish <https://translate.fedoraproject.org/projects/" "pipewire/pipewire/tr/>\n" "Language: tr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Weblate 4.4.2\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Dahili Ses" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Modem" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Bilinmeyen aygıt" 07070100000059000081A40000000000000000000000016178A88C00000424000000000000000000000000000000000000001D00000000media-session-0.4.1/po/uk.po# Copyright (C) 2009 Free Software Foundation, Inc. # This file is distributed under the same license as the pipewire package. # # Yuri Chornoivan <yurchor@ukr.net>, 2009-2021. msgid "" msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-09-23 12:37+0300\n" "Last-Translator: Yuri Chornoivan <yurchor@ukr.net>\n" "Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Lokalize 20.12.0\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "Вбудоване аудіо" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "Модем" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "Невідомий пристрій" 0707010000005A000081A40000000000000000000000016178A88C000004BC000000000000000000000000000000000000002000000000media-session-0.4.1/po/zh_CN.po# Simplified Chinese translation for PipeWire. # Copyright (C) 2008 PULSEAUDIO COPYRIGHT HOLDER # This file is distributed under the same license as the pipewire package. # 闫丰刚 <sainry@gmail.com>, 2008, 2009. # Leah Liu <lliu@redhat.com>, 2009, 2012. # Cheng-Chia Tseng <pswo10680@gmail.com>, 2010, 2012. # Frank Hill <hxf.prc@gmail.com>, 2015. # Mingye Wang (Arthur2e5) <arthur200126@gmail.com>, 2015. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2021-04-18 10:56+0800\n" "Last-Translator: Huang-Huang Bao <i@eh5.me>\n" "Language-Team: Huang-Huang Bao <i@eh5.me>\n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-03-22 13:23+0000\n" "X-Generator: Poedit 2.4.1\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "内置音频" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "调制解调器" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "未知设备" 0707010000005B000081A40000000000000000000000016178A88C000003FC000000000000000000000000000000000000002000000000media-session-0.4.1/po/zh_TW.po# Chinese (Taiwan) translation for pipewire. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # # Cheng-Chia Tseng <pswo10680@gmail.com>, 2010, 2012. # pan93412 <pan93412@gmail.com>, 2020. msgid "" msgstr "" "Project-Id-Version: PipeWire Volume Control\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire-media-" "session/issues/new\n" "POT-Creation-Date: 2021-10-20 10:03+1000\n" "PO-Revision-Date: 2020-01-11 13:49+0800\n" "Last-Translator: pan93412 <pan93412@gmail.com>\n" "Language-Team: Chinese <zh-l10n@lists.linux.org.tw>\n" "Language: zh_TW\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Lokalize 19.12.0\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/alsa-monitor.c:662 msgid "Built-in Audio" msgstr "內部音效" #: src/alsa-monitor.c:666 msgid "Modem" msgstr "數據機" #: src/alsa-monitor.c:675 msgid "Unknown device" msgstr "" 0707010000005C000041ED0000000000000000000000026178A88C00000000000000000000000000000000000000000000001800000000media-session-0.4.1/src0707010000005D000081A40000000000000000000000016178A88C00001780000000000000000000000000000000000000002900000000media-session-0.4.1/src/access-flatpak.c/* PipeWire * * Copyright © 2020 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <spa/utils/string.h> #include "pipewire/pipewire.h" #include "media-session.h" /** \page page_media_session_module_access_flatpak Media Session Module: Access Flatpak * * The Access Flatpak module manages permissions for flatpak clients. It * monitors clients with a \ref PW_KEY_ACCESS value of `"flatpak"` and * restricts those clients to the \ref PW_PERM_R and \ref PW_PERM_X * permissions. * * Clients of \ref PW_KEY_MEDIA_CATEGORY type "Manager" are permitted full * access (\ref PW_PERM_ALL). * * The value "flatpak" is typically assigned by the \ref page_module_access. * * ## Module Properties * * This module requires the following properties on the client object: * - \ref PW_KEY_ACCESS * - \ref PW_KEY_MEDIA_CATEGORY (optional, only matters for "Manager" objects) */ #define NAME "access-flatpak" #define SESSION_KEY "access-flatpak" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct impl { struct sm_media_session *session; struct spa_hook listener; struct spa_list client_list; }; struct client { struct sm_client *obj; uint32_t id; struct impl *impl; struct spa_list link; /**< link in impl client_list */ struct spa_hook listener; unsigned int active:1; }; static void object_update(void *data) { struct client *client = data; struct impl *impl = client->impl; const char *str; pw_log_debug("%p: client %p %08x", impl, client, client->obj->obj.changed); if (client->obj->obj.avail & SM_CLIENT_CHANGE_MASK_INFO && !client->active) { struct pw_permission permissions[1]; uint32_t perms; if (client->obj->info == NULL || client->obj->info->props == NULL || (str = spa_dict_lookup(client->obj->info->props, PW_KEY_ACCESS)) == NULL || !spa_streq(str, "flatpak")) return; if ((str = spa_dict_lookup(client->obj->info->props, PW_KEY_MEDIA_CATEGORY)) != NULL && (spa_streq(str, "Manager"))) { /* FIXME, use permission store to check if this app is allowed to * be a manager app */ perms = PW_PERM_ALL; } else { /* limited access for everything else */ perms = PW_PERM_R | PW_PERM_X; } pw_log_info("%p: flatpak client %d granted 0x%08x permissions" , impl, client->id, perms); permissions[0] = PW_PERMISSION_INIT(PW_ID_ANY, perms); pw_client_update_permissions(client->obj->obj.proxy, 1, permissions); client->active = true; } } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static int handle_client(struct impl *impl, struct sm_object *object) { struct client *client; pw_log_debug("%p: new client '%u'", impl, object->id); client = sm_object_add_data(object, SESSION_KEY, sizeof(struct client)); client->obj = (struct sm_client*)object; client->id = object->id; client->impl = impl; spa_list_append(&impl->client_list, &client->link); client->obj->obj.mask |= SM_CLIENT_CHANGE_MASK_INFO; sm_object_add_listener(&client->obj->obj, &client->listener, &object_events, client); return 1; } static void destroy_client(struct impl *impl, struct client *client) { spa_list_remove(&client->link); spa_hook_remove(&client->listener); sm_object_remove_data((struct sm_object*)client->obj, SESSION_KEY); } static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; int res; pw_log_debug("%p: create global '%d'", impl, object->id); if (spa_streq(object->type, PW_TYPE_INTERFACE_Client)) res = handle_client(impl, object); else res = 0; if (res < 0) pw_log_warn("%p: can't handle global %d", impl, object->id); } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; pw_log_debug("%p: remove global '%d'", impl, object->id); if (spa_streq(object->type, PW_TYPE_INTERFACE_Client)) { struct client *client; if ((client = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_client(impl, client); } } static void session_destroy(void *data) { struct impl *impl = data; struct client *client; spa_list_consume(client, &impl->client_list, link) destroy_client(impl, client); spa_hook_remove(&impl->listener); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .destroy = session_destroy, }; int sm_access_flatpak_start(struct sm_media_session *session) { struct impl *impl; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; spa_list_init(&impl->client_list); sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); return 0; } 0707010000005E000081A40000000000000000000000016178A88C00004774000000000000000000000000000000000000002800000000media-session-0.4.1/src/access-portal.c/* PipeWire * * Copyright © 2020 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <dbus/dbus.h> #include <spa/utils/string.h> #include <spa/support/dbus.h> #include <spa/debug/dict.h> #include "pipewire/pipewire.h" #include "media-session.h" /** \page page_media_session_module_access_portal Media Session Module: Access Portal * * The Access Portal module manages media roles for clients * started through a portal (see \ref page_module_portal). Clients must have a * \ref PW_KEY_ACCESS or \ref PW_KEY_CLIENT_ACCESS property value of * `"portal"`, all other clients are ignored. The portal is expected to assign *`"pipewire.access.portal.media_roles"` to this client, these roles are * checked against the * [org.freedesktop.impl.portal.PermissionStore](https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-org.freedesktop.impl.portal.PermissionStore). * Where permitted, the resulting client media role becomes the permitted * set of roles. * * @note Currently only the "Camera" media role is supported. * * The Permission Store entry table used by this module is `"devices"`, the * resource ID is `"camera"`. * * ## Module Properties * * This module requires the following properties on the client object: * * - `"pipewire.access.portal.is_portal"`: set to `"true"` for the portal * client itself, empty or `"false"` otherwise * - `"pipewire.access.portal.app_id"`: the application ID of the client * - `"pipewire.access.portal.media_roles"` the media roles that should be * assigned to this client (if permitted by the PermissionStore). * * The above properties must be set by the portal initiating the client * connection. * * See e.g. the [xdg-desktop-portal](github.com/flatpak/xdg-desktop-portal) * for an implementation that sets the above properties. It creates multiple * connections to pipewire, one with `is_portal` set to true for the portal * itself and one connection per application request to open the camera. The * latter have `app_id` and `media_roles` set accordingly. */ #define NAME "access-portal" #define SESSION_KEY "access-portal" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic enum media_role { MEDIA_ROLE_INVALID = -1, MEDIA_ROLE_NONE = 0, MEDIA_ROLE_CAMERA = 1 << 0, }; #define MEDIA_ROLE_ALL (MEDIA_ROLE_CAMERA) struct impl { struct sm_media_session *session; struct spa_hook listener; struct spa_list client_list; DBusConnection *bus; }; struct client { struct impl *impl; struct sm_client *obj; struct spa_hook listener; struct spa_list link; /**< link in impl client_list */ uint32_t id; unsigned int portal_managed:1; unsigned int setup_complete:1; unsigned int is_portal:1; char *app_id; enum media_role media_roles; enum media_role allowed_media_roles; }; static DBusConnection *get_dbus_connection(struct impl *impl); static void client_info_changed(struct client *client, const struct pw_client_info *info); static enum media_role media_role_from_string(const char *media_role_str) { if (spa_streq(media_role_str, "Camera")) return MEDIA_ROLE_CAMERA; else return MEDIA_ROLE_INVALID; } static enum media_role parse_media_roles(const char *media_types_str) { enum media_role media_roles = 0; char *buf_orig; char *buf; buf_orig = strdup(media_types_str); buf = buf_orig; while (buf) { char *media_role_str; enum media_role media_role; media_role_str = buf; strsep(&buf, ","); media_role = media_role_from_string(media_role_str); if (media_role != MEDIA_ROLE_INVALID) { media_roles |= MEDIA_ROLE_CAMERA; } else { pw_log_debug("Client specified unknown media role '%s'", media_role_str); } } free(buf_orig); return media_roles; } static enum media_role media_role_from_properties(const struct pw_properties *props) { const char *media_class_str; const char *media_role_str; media_class_str = pw_properties_get(props, "media.class"); media_role_str = pw_properties_get(props, "media.role"); if (media_class_str == NULL) return MEDIA_ROLE_INVALID; if (media_role_str == NULL) return MEDIA_ROLE_INVALID; if (!spa_streq(media_class_str, "Video/Source")) return MEDIA_ROLE_INVALID; return media_role_from_string(media_role_str); } static void object_update(void *data) { struct client *client = data; struct impl *impl = client->impl; pw_log_debug("%p: client %p %08x", impl, client, client->obj->obj.changed); if (client->obj->obj.avail & SM_CLIENT_CHANGE_MASK_INFO) client_info_changed(client, client->obj->info); } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static int handle_client(struct impl *impl, struct sm_object *object) { struct client *client; const char *str; pw_log_debug("%p: new client '%u'", impl, object->id); client = sm_object_add_data(object, SESSION_KEY, sizeof(struct client)); client->obj = (struct sm_client*)object; client->id = object->id; client->impl = impl; spa_list_append(&impl->client_list, &client->link); client->obj->obj.mask |= SM_CLIENT_CHANGE_MASK_INFO; sm_object_add_listener(&client->obj->obj, &client->listener, &object_events, client); if (((str = pw_properties_get(client->obj->obj.props, PW_KEY_ACCESS)) != NULL || (str = pw_properties_get(client->obj->obj.props, PW_KEY_CLIENT_ACCESS)) != NULL) && spa_streq(str, "portal")) { client->portal_managed = true; pw_log_info("%p: portal-managed client %d added", impl, client->id); } return 1; } static int set_global_permissions(void *data, struct sm_object *object) { struct client *client = data; struct impl *impl = client->impl; struct pw_permission permissions[1]; const struct pw_properties *props; int n_permissions = 0; bool set_permission; bool allowed = false; if ((props = object->props) == NULL) return 0; pw_log_debug("%p: object %d type:%s", impl, object->id, object->type); if (spa_streq(object->type, PW_TYPE_INTERFACE_Client)) { set_permission = allowed = object->id == client->id; } else if (spa_streq(object->type, PW_TYPE_INTERFACE_Node)) { enum media_role media_role; media_role = media_role_from_properties(props); if (media_role == MEDIA_ROLE_INVALID) { set_permission = false; } else if (client->allowed_media_roles & media_role) { set_permission = true; allowed = true; } else if (client->media_roles & media_role) { set_permission = true; allowed = false; } else { set_permission = false; } } else { set_permission = false; } if (set_permission) { permissions[n_permissions++] = PW_PERMISSION_INIT(object->id, allowed ? PW_PERM_ALL : 0); pw_log_info("%p: object %d allowed:%d", impl, object->id, allowed); pw_client_update_permissions(client->obj->obj.proxy, n_permissions, permissions); } return 0; } static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; pw_log_debug("%p: create global '%d'", impl, object->id); if (spa_streq(object->type, PW_TYPE_INTERFACE_Client)) { handle_client(impl, object); } else { struct client *client; spa_list_for_each(client, &impl->client_list, link) { if (client->portal_managed && !client->is_portal) set_global_permissions(client, object); } } } static void destroy_client(struct impl *impl, struct client *client) { spa_list_remove(&client->link); spa_hook_remove(&client->listener); free(client->app_id); sm_object_remove_data((struct sm_object*)client->obj, SESSION_KEY); } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; pw_log_debug("%p: remove global '%d'", impl, object->id); if (spa_streq(object->type, PW_TYPE_INTERFACE_Client)) { struct client *client; if ((client = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_client(impl, client); } } static void session_destroy(void *data) { struct impl *impl = data; struct client *client; spa_list_consume(client, &impl->client_list, link) destroy_client(impl, client); spa_hook_remove(&impl->listener); free(impl); } static void session_dbus_disconnected(void *data) { struct impl *impl = data; if (impl->bus) dbus_connection_unref(impl->bus); impl->bus = NULL; } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .destroy = session_destroy, .dbus_disconnected = session_dbus_disconnected, }; static bool check_permission_allowed(DBusMessageIter *iter) { bool allowed = false; while (dbus_message_iter_get_arg_type (iter) != DBUS_TYPE_INVALID) { const char *permission_value; dbus_message_iter_get_basic(iter, &permission_value); if (spa_streq(permission_value, "yes")) { allowed = true; break; } dbus_message_iter_next(iter); } return allowed; } static void do_permission_store_check(struct client *client) { struct impl *impl = client->impl; DBusMessage *m = NULL, *r = NULL; DBusError error; DBusMessageIter msg_iter; const char *table; const char *id; DBusMessageIter r_iter; DBusMessageIter permissions_iter; DBusConnection *bus; if (client->app_id == NULL) { pw_log_debug("Ignoring portal check for broken portal managed client %p", client); goto err_not_allowed; } if (client->media_roles == 0) { pw_log_debug("Ignoring portal check for portal client %p with static permissions", client); sm_media_session_for_each_object(impl->session, set_global_permissions, client); return; } if (spa_streq(client->app_id, "")) { pw_log_debug("Ignoring portal check for non-sandboxed portal client %p", client); client->allowed_media_roles = MEDIA_ROLE_ALL; sm_media_session_for_each_object(impl->session, set_global_permissions, client); return; } bus = get_dbus_connection(impl); if (bus == NULL) { pw_log_debug("Ignoring portal check for client %p: no dbus", client); client->allowed_media_roles = MEDIA_ROLE_ALL; sm_media_session_for_each_object(impl->session, set_global_permissions, client); return; } client->allowed_media_roles = MEDIA_ROLE_NONE; dbus_error_init(&error); m = dbus_message_new_method_call("org.freedesktop.impl.portal.PermissionStore", "/org/freedesktop/impl/portal/PermissionStore", "org.freedesktop.impl.portal.PermissionStore", "Lookup"); dbus_message_iter_init_append(m, &msg_iter); table = "devices"; dbus_message_iter_append_basic(&msg_iter, DBUS_TYPE_STRING, &table); id = "camera"; dbus_message_iter_append_basic(&msg_iter, DBUS_TYPE_STRING, &id); if (!(r = dbus_connection_send_with_reply_and_block(bus, m, -1, &error))) { pw_log_error("Failed to call permission store: %s", error.message); dbus_error_free(&error); goto err_not_allowed; } dbus_message_unref(m); dbus_message_iter_init(r, &r_iter); dbus_message_iter_recurse(&r_iter, &permissions_iter); while (dbus_message_iter_get_arg_type(&permissions_iter) != DBUS_TYPE_INVALID) { DBusMessageIter permissions_entry_iter; const char *app_id; DBusMessageIter permission_values_iter; bool camera_allowed; dbus_message_iter_recurse(&permissions_iter, &permissions_entry_iter); dbus_message_iter_get_basic(&permissions_entry_iter, &app_id); pw_log_info("permissions %s", app_id); if (!spa_streq(app_id, client->app_id)) { dbus_message_iter_next(&permissions_iter); continue; } dbus_message_iter_next(&permissions_entry_iter); dbus_message_iter_recurse(&permissions_entry_iter, &permission_values_iter); camera_allowed = check_permission_allowed(&permission_values_iter); pw_log_info("allowed %d", camera_allowed); client->allowed_media_roles |= camera_allowed ? MEDIA_ROLE_CAMERA : MEDIA_ROLE_NONE; sm_media_session_for_each_object(impl->session, set_global_permissions, client); break; } dbus_message_unref(r); return; err_not_allowed: return; } static void client_info_changed(struct client *client, const struct pw_client_info *info) { struct impl *impl = client->impl; const struct spa_dict *props; const char *is_portal; const char *app_id; const char *media_roles; if (!client->portal_managed || client->is_portal) return; if (client->setup_complete) return; if ((props = info->props) == NULL) { pw_log_error("Portal managed client didn't have any properties"); return; } is_portal = spa_dict_lookup(props, "pipewire.access.portal.is_portal"); if (spa_streq(is_portal, "yes") || pw_properties_parse_bool(is_portal)) { pw_log_info("%p: client %d is the portal itself", impl, client->id); client->is_portal = true; return; }; app_id = spa_dict_lookup(props, "pipewire.access.portal.app_id"); if (app_id == NULL) { pw_log_error("%p: Portal managed client %d didn't set app_id", impl, client->id); return; } media_roles = spa_dict_lookup(props, "pipewire.access.portal.media_roles"); if (media_roles == NULL) { pw_log_error("%p: Portal managed client %d didn't set media_roles", impl, client->id); return; } client->app_id = strdup(app_id); client->media_roles = parse_media_roles(media_roles); pw_log_info("%p: client %d with app_id '%s' set to portal access", impl, client->id, client->app_id); do_permission_store_check(client); client->setup_complete = true; } static DBusHandlerResult permission_store_changed_handler(DBusConnection *connection, DBusMessage *message, void *user_data) { struct impl *impl = user_data; struct client *client; DBusMessageIter iter; const char *table; const char *id; dbus_bool_t deleted; DBusMessageIter permissions_iter; if (!dbus_message_is_signal(message, "org.freedesktop.impl.portal.PermissionStore", "Changed")) return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; spa_list_for_each(client, &impl->client_list, link) { if (!client->portal_managed) continue; client->allowed_media_roles = MEDIA_ROLE_NONE; } dbus_message_iter_init(message, &iter); dbus_message_iter_get_basic(&iter, &table); dbus_message_iter_next(&iter); dbus_message_iter_get_basic(&iter, &id); if (!spa_streq(table, "devices") || !spa_streq(id, "camera")) return DBUS_HANDLER_RESULT_HANDLED; dbus_message_iter_next(&iter); dbus_message_iter_get_basic(&iter, &deleted); dbus_message_iter_next(&iter); /* data variant (ignored) */ dbus_message_iter_next(&iter); dbus_message_iter_recurse(&iter, &permissions_iter); while (dbus_message_iter_get_arg_type(&permissions_iter) != DBUS_TYPE_INVALID) { DBusMessageIter permissions_entry_iter; const char *app_id; DBusMessageIter permission_values_iter; bool camera_allowed; dbus_message_iter_recurse(&permissions_iter, &permissions_entry_iter); dbus_message_iter_get_basic(&permissions_entry_iter, &app_id); dbus_message_iter_next(&permissions_entry_iter); dbus_message_iter_recurse(&permissions_entry_iter, &permission_values_iter); camera_allowed = check_permission_allowed(&permission_values_iter); spa_list_for_each(client, &impl->client_list, link) { if (!client->portal_managed) continue; if (client->is_portal) continue; if (client->app_id == NULL || !spa_streq(client->app_id, app_id)) continue; if (!(client->media_roles & MEDIA_ROLE_CAMERA)) continue; if (camera_allowed) client->allowed_media_roles |= MEDIA_ROLE_CAMERA; sm_media_session_for_each_object(impl->session, set_global_permissions, client); } dbus_message_iter_next(&permissions_iter); } return DBUS_HANDLER_RESULT_HANDLED; } static DBusConnection *get_dbus_connection(struct impl *impl) { struct sm_media_session *session = impl->session; DBusError error; if (impl->bus) return impl->bus; if (session->dbus_connection) impl->bus = spa_dbus_connection_get(session->dbus_connection); if (impl->bus == NULL) { pw_log_warn("no dbus connection, portal access disabled"); return NULL; } pw_log_debug("got dbus connection %p", impl->bus); dbus_error_init(&error); dbus_bus_add_match(impl->bus, "type='signal',\ sender='org.freedesktop.impl.portal.PermissionStore',\ interface='org.freedesktop.impl.portal.PermissionStore',\ member='Changed'", &error); if (dbus_error_is_set(&error)) { pw_log_error("Failed to add permission store changed listener: %s", error.message); dbus_error_free(&error); impl->bus = NULL; return NULL; } dbus_connection_ref(impl->bus); dbus_connection_add_filter(impl->bus, permission_store_changed_handler, impl, NULL); return impl->bus; } int sm_access_portal_start(struct sm_media_session *session) { struct impl *impl; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; spa_list_init(&impl->client_list); impl->session = session; get_dbus_connection(impl); sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); return 0; } 0707010000005F000081A40000000000000000000000016178A88C000051DC000000000000000000000000000000000000002800000000media-session-0.4.1/src/alsa-endpoint.c/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <alsa/asoundlib.h> #include <alsa/use-case.h> #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/utils/result.h> #include <spa/utils/names.h> #include <spa/utils/keys.h> #include <spa/utils/string.h> #include <spa/param/audio/format-utils.h> #include <spa/param/props.h> #include <spa/debug/dict.h> #include <spa/debug/pod.h> #include "pipewire/pipewire.h" #include <pipewire/extensions/session-manager.h> #include "media-session.h" /** \page page_media_session_module_alsa_endpoint Media Session Module: ALSA Endpoint * * The ALSA endpoint module creates an endpoint and corresponding endpoint * stream for each Node on ALSA devices. * * ALSA devices are defined as devices with a \ref PW_KEY_MEDIA_CLASS * starting with `"Audio/"` and a \ref PW_KEY_DEVICE_API of `"alsa"`. */ #define NAME "alsa-endpoint" #define SESSION_KEY "alsa-endpoint" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct endpoint { struct spa_list link; struct impl *impl; struct pw_properties *props; struct node *node; struct spa_hook listener; struct pw_client_endpoint *client_endpoint; struct spa_hook proxy_listener; struct spa_hook client_endpoint_listener; struct pw_endpoint_info info; struct spa_param_info params[5]; struct endpoint *monitor; unsigned int use_ucm:1; snd_use_case_mgr_t *ucm; struct spa_audio_info format; struct spa_list stream_list; }; struct stream { struct spa_list link; struct endpoint *endpoint; struct pw_properties *props; struct pw_endpoint_stream_info info; struct spa_audio_info format; unsigned int active:1; }; struct node { struct impl *impl; struct sm_node *node; struct device *device; struct endpoint *endpoint; }; struct device { struct impl *impl; uint32_t id; struct sm_device *device; struct spa_hook listener; struct spa_list endpoint_list; }; struct impl { struct sm_media_session *session; struct spa_hook listener; }; static int client_endpoint_set_session_id(void *object, uint32_t id) { struct endpoint *endpoint = object; endpoint->info.session_id = id; return 0; } static int client_endpoint_set_param(void *object, uint32_t id, uint32_t flags, const struct spa_pod *param) { struct endpoint *endpoint = object; struct impl *impl = endpoint->impl; pw_log_debug("%p: endpoint %p set param %d", impl, endpoint, id); return pw_node_set_param((struct pw_node*)endpoint->node->node->obj.proxy, id, flags, param); } static int client_endpoint_stream_set_param(void *object, uint32_t stream_id, uint32_t id, uint32_t flags, const struct spa_pod *param) { return -ENOTSUP; } static int stream_set_active(struct endpoint *endpoint, struct stream *stream, bool active) { char buf[1024]; struct spa_pod_builder b = { 0, }; struct spa_pod *param; if (stream->active == active) return 0; if (active) { stream->format.info.raw.rate = 48000; spa_pod_builder_init(&b, buf, sizeof(buf)); param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &stream->format.info.raw); param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(endpoint->info.direction), SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp), SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(true), SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, param); pw_node_set_param((struct pw_node*)endpoint->node->node->obj.proxy, SPA_PARAM_PortConfig, 0, param); } stream->active = active; return 0; } static int client_endpoint_create_link(void *object, const struct spa_dict *props) { struct endpoint *endpoint = object; struct impl *impl = endpoint->impl; struct pw_properties *p; int res; pw_log_debug("%p: endpoint %p", impl, endpoint); if (props == NULL) return -EINVAL; p = pw_properties_new_dict(props); if (p == NULL) return -errno; if (endpoint->info.direction == PW_DIRECTION_OUTPUT) { const char *str; struct sm_object *obj; str = spa_dict_lookup(props, PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT); if (str == NULL) { pw_log_warn("%p: no target endpoint given", impl); res = -EINVAL; goto exit; } obj = sm_media_session_find_object(impl->session, atoi(str)); if (obj == NULL || !spa_streq(obj->type, PW_TYPE_INTERFACE_Endpoint)) { pw_log_warn("%p: could not find endpoint %s (%p)", impl, str, obj); res = -EINVAL; goto exit; } pw_properties_setf(p, PW_KEY_LINK_OUTPUT_NODE, "%d", endpoint->node->node->info->id); pw_properties_setf(p, PW_KEY_LINK_OUTPUT_PORT, "-1"); pw_endpoint_create_link((struct pw_endpoint*)obj->proxy, &p->dict); } else { pw_properties_setf(p, PW_KEY_LINK_INPUT_NODE, "%d", endpoint->node->node->info->id); pw_properties_setf(p, PW_KEY_LINK_INPUT_PORT, "-1"); sm_media_session_create_links(impl->session, &p->dict); } res = 0; exit: pw_properties_free(p); return res; } static const struct pw_client_endpoint_events client_endpoint_events = { PW_VERSION_CLIENT_ENDPOINT_EVENTS, .set_session_id = client_endpoint_set_session_id, .set_param = client_endpoint_set_param, .stream_set_param = client_endpoint_stream_set_param, .create_link = client_endpoint_create_link, }; static struct stream *endpoint_add_stream(struct endpoint *endpoint) { struct stream *s; const char *str; s = calloc(1, sizeof(*s)); if (s == NULL) return NULL; s->props = pw_properties_new(NULL, NULL); s->endpoint = endpoint; if ((str = pw_properties_get(endpoint->props, PW_KEY_MEDIA_CLASS)) != NULL) pw_properties_set(s->props, PW_KEY_MEDIA_CLASS, str); if ((str = pw_properties_get(endpoint->props, PW_KEY_PRIORITY_SESSION)) != NULL) pw_properties_set(s->props, PW_KEY_PRIORITY_SESSION, str); if (endpoint->info.direction == PW_DIRECTION_OUTPUT) { if (endpoint->monitor != NULL) pw_properties_set(s->props, PW_KEY_ENDPOINT_STREAM_NAME, "Monitor"); else pw_properties_set(s->props, PW_KEY_ENDPOINT_STREAM_NAME, "Capture"); } else { pw_properties_set(s->props, PW_KEY_ENDPOINT_STREAM_NAME, "Playback"); } s->info.version = PW_VERSION_ENDPOINT_STREAM_INFO; s->info.id = endpoint->info.n_streams; s->info.endpoint_id = endpoint->info.id; s->info.name = (char*)pw_properties_get(s->props, PW_KEY_ENDPOINT_STREAM_NAME); s->info.change_mask = PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS; s->info.props = &s->props->dict; s->format = endpoint->format; pw_log_debug("stream %d", s->info.id); pw_client_endpoint_stream_update(endpoint->client_endpoint, s->info.id, PW_CLIENT_ENDPOINT_STREAM_UPDATE_INFO, 0, NULL, &s->info); spa_list_append(&endpoint->stream_list, &s->link); endpoint->info.n_streams++; return s; } static void destroy_stream(struct stream *stream) { struct endpoint *endpoint = stream->endpoint; pw_client_endpoint_stream_update(endpoint->client_endpoint, stream->info.id, PW_CLIENT_ENDPOINT_STREAM_UPDATE_DESTROYED, 0, NULL, &stream->info); spa_list_remove(&stream->link); endpoint->info.n_streams--; pw_properties_free(stream->props); free(stream); } static void update_params(void *data) { uint32_t n_params; const struct spa_pod **params; struct endpoint *endpoint = data; struct sm_node *node = endpoint->node->node; struct sm_param *p; pw_log_debug("%p: endpoint", endpoint); params = alloca(sizeof(struct spa_pod *) * node->n_params); n_params = 0; spa_list_for_each(p, &node->param_list, link) { switch (p->id) { case SPA_PARAM_Props: case SPA_PARAM_PropInfo: params[n_params++] = p->param; break; default: break; } } pw_client_endpoint_update(endpoint->client_endpoint, PW_CLIENT_ENDPOINT_UPDATE_PARAMS | PW_CLIENT_ENDPOINT_UPDATE_INFO, n_params, params, &endpoint->info); } static struct endpoint *create_endpoint(struct node *node, struct endpoint *monitor); static void object_update(void *data) { struct endpoint *endpoint = data; struct impl *impl = endpoint->impl; struct sm_node *node = endpoint->node->node; pw_log_debug("%p: endpoint %p", impl, endpoint); if (node->obj.changed & SM_NODE_CHANGE_MASK_PARAMS) update_params(endpoint); } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static void complete_endpoint(void *data) { struct endpoint *endpoint = data; struct stream *stream; struct sm_param *p; pw_log_debug("endpoint %p: complete", endpoint); spa_list_for_each(p, &endpoint->node->node->param_list, link) { struct spa_audio_info info = { 0, }; if (p->id != SPA_PARAM_EnumFormat) continue; if (spa_format_parse(p->param, &info.media_type, &info.media_subtype) < 0) continue; if (info.media_type != SPA_MEDIA_TYPE_audio || info.media_subtype != SPA_MEDIA_SUBTYPE_raw) continue; spa_pod_object_fixate((struct spa_pod_object*)p->param); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, p->param); if (spa_format_audio_raw_parse(p->param, &info.info.raw) < 0) continue; if (endpoint->format.info.raw.channels < info.info.raw.channels) endpoint->format = info; } pw_client_endpoint_update(endpoint->client_endpoint, PW_CLIENT_ENDPOINT_UPDATE_INFO, 0, NULL, &endpoint->info); stream = endpoint_add_stream(endpoint); if (endpoint->info.direction == PW_DIRECTION_INPUT) { struct endpoint *monitor; /* make monitor for sinks */ monitor = create_endpoint(endpoint->node, endpoint); if (monitor == NULL) return; endpoint_add_stream(monitor); } stream_set_active(endpoint, stream, true); sm_object_add_listener(&endpoint->node->node->obj, &endpoint->listener, &object_events, endpoint); } static void proxy_destroy(void *data) { struct endpoint *endpoint = data; struct stream *s; spa_list_consume(s, &endpoint->stream_list, link) destroy_stream(s); pw_properties_free(endpoint->props); spa_list_remove(&endpoint->link); spa_hook_remove(&endpoint->proxy_listener); spa_hook_remove(&endpoint->client_endpoint_listener); endpoint->client_endpoint = NULL; } static void proxy_bound(void *data, uint32_t id) { struct endpoint *endpoint = data; endpoint->info.id = id; } static const struct pw_proxy_events proxy_events = { PW_VERSION_PROXY_EVENTS, .destroy = proxy_destroy, .bound = proxy_bound, }; static struct endpoint *create_endpoint(struct node *node, struct endpoint *monitor) { struct impl *impl = node->impl; struct device *device = node->device; struct pw_properties *props; struct endpoint *endpoint; struct pw_proxy *proxy; const char *str, *media_class = NULL, *name = NULL; uint32_t subscribe[4], n_subscribe = 0; struct pw_properties *pr = node->node->obj.props; enum pw_direction direction; if (pr == NULL) { errno = EINVAL; return NULL; } if ((media_class = pw_properties_get(pr, PW_KEY_MEDIA_CLASS)) == NULL) { errno = EINVAL; return NULL; } if (strstr(media_class, "Source") != NULL) { direction = PW_DIRECTION_OUTPUT; } else if (strstr(media_class, "Sink") != NULL) { direction = PW_DIRECTION_INPUT; } else { errno = EINVAL; return NULL; } props = pw_properties_new(NULL, NULL); if (props == NULL) return NULL; if (monitor != NULL) { pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source"); direction = PW_DIRECTION_OUTPUT; } else { pw_properties_set(props, PW_KEY_MEDIA_CLASS, media_class); } if ((str = pw_properties_get(pr, PW_KEY_PRIORITY_SESSION)) != NULL) pw_properties_set(props, PW_KEY_PRIORITY_SESSION, str); if ((name = pw_properties_get(pr, PW_KEY_NODE_DESCRIPTION)) != NULL) { if (monitor != NULL) { pw_properties_setf(props, PW_KEY_ENDPOINT_NAME, "Monitor of %s", monitor->info.name); pw_properties_setf(props, PW_KEY_ENDPOINT_MONITOR, "%d", monitor->info.id); } else { pw_properties_set(props, PW_KEY_ENDPOINT_NAME, name); } } if ((str = pw_properties_get(pr, PW_KEY_DEVICE_ICON_NAME)) != NULL) pw_properties_set(props, PW_KEY_ENDPOINT_ICON_NAME, str); proxy = sm_media_session_create_object(impl->session, "client-endpoint", PW_TYPE_INTERFACE_ClientEndpoint, PW_VERSION_CLIENT_ENDPOINT, &props->dict, sizeof(*endpoint)); if (proxy == NULL) { pw_properties_free(props); return NULL; } endpoint = pw_proxy_get_user_data(proxy); endpoint->impl = impl; endpoint->node = node; endpoint->monitor = monitor; endpoint->props = props; endpoint->client_endpoint = (struct pw_client_endpoint *) proxy; endpoint->info.version = PW_VERSION_ENDPOINT_INFO; endpoint->info.name = (char*)pw_properties_get(endpoint->props, PW_KEY_ENDPOINT_NAME); endpoint->info.media_class = (char*)pw_properties_get(endpoint->props, PW_KEY_MEDIA_CLASS); endpoint->info.session_id = impl->session->session->obj.id; endpoint->info.direction = direction; endpoint->info.flags = 0; endpoint->info.change_mask = PW_ENDPOINT_CHANGE_MASK_STREAMS | PW_ENDPOINT_CHANGE_MASK_SESSION | PW_ENDPOINT_CHANGE_MASK_PROPS | PW_ENDPOINT_CHANGE_MASK_PARAMS; endpoint->info.n_streams = 0; endpoint->info.props = &endpoint->props->dict; endpoint->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); endpoint->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); endpoint->info.params = endpoint->params; endpoint->info.n_params = 2; spa_list_init(&endpoint->stream_list); pw_log_debug("%p: new endpoint %p for alsa node %p", impl, endpoint, node); pw_proxy_add_listener(proxy, &endpoint->proxy_listener, &proxy_events, endpoint); pw_client_endpoint_add_listener(endpoint->client_endpoint, &endpoint->client_endpoint_listener, &client_endpoint_events, endpoint); subscribe[n_subscribe++] = SPA_PARAM_EnumFormat; subscribe[n_subscribe++] = SPA_PARAM_Props; subscribe[n_subscribe++] = SPA_PARAM_PropInfo; pw_log_debug("%p: endpoint %p proxy %p subscribe %d params", impl, endpoint, node->node->obj.proxy, n_subscribe); pw_node_subscribe_params((struct pw_node*)node->node->obj.proxy, subscribe, n_subscribe); spa_list_append(&device->endpoint_list, &endpoint->link); if (monitor == NULL) sm_media_session_sync(impl->session, complete_endpoint, endpoint); return endpoint; } static void destroy_endpoint(struct endpoint *endpoint) { if (endpoint->client_endpoint) pw_proxy_destroy((struct pw_proxy*)endpoint->client_endpoint); } /** fallback, one stream for each node */ static int setup_alsa_fallback_endpoint(struct device *device) { struct impl *impl = device->impl; struct sm_node *n; struct sm_device *d = device->device; pw_log_debug("%p: device %p fallback", impl, d); spa_list_for_each(n, &d->node_list, link) { struct node *node; pw_log_debug("%p: device %p has node %p", impl, d, n); node = sm_object_add_data(&n->obj, SESSION_KEY, sizeof(struct node)); node->device = device; node->node = n; node->impl = impl; node->endpoint = create_endpoint(node, NULL); if (node->endpoint == NULL) return -errno; } return 0; } /** UCM. * * We create 1 stream for each verb + modifier combination */ static int setup_alsa_ucm_endpoint(struct device *device) { const char *str, *card_name = NULL; char *name_free = NULL; int i, res, num_verbs; const char **verb_list = NULL; struct spa_dict *props = device->device->info->props; snd_use_case_mgr_t *ucm; card_name = spa_dict_lookup(props, SPA_KEY_API_ALSA_CARD_NAME); if (card_name == NULL && (str = spa_dict_lookup(props, SPA_KEY_API_ALSA_CARD)) != NULL) { snd_card_get_name(atoi(str), &name_free); card_name = name_free; pw_log_debug("got card name %s for index %s", card_name, str); } if (card_name == NULL) { pw_log_error("can't get card name for index %s", str); res = -ENOTSUP; goto exit; } if ((res = snd_use_case_mgr_open(&ucm, card_name)) < 0) { pw_log_error("can not open UCM for %s: %s", card_name, snd_strerror(res)); goto exit; } num_verbs = snd_use_case_verb_list(ucm, &verb_list); if (num_verbs < 0) { res = num_verbs; pw_log_error("UCM verb list not found for %s: %s", card_name, snd_strerror(num_verbs)); goto close_exit; } for (i = 0; i < num_verbs; i++) { pw_log_debug("verb: %s", verb_list[i]); } /* FIXME: implement this */ snd_use_case_free_list(verb_list, num_verbs); res = -ENOTSUP; close_exit: snd_use_case_mgr_close(ucm); exit: free(name_free); return res; } static int activate_device(struct device *device) { int res; if ((res = setup_alsa_ucm_endpoint(device)) < 0) res = setup_alsa_fallback_endpoint(device); return res; } static int deactivate_device(struct device *device) { struct endpoint *e; spa_list_consume(e, &device->endpoint_list, link) destroy_endpoint(e); return 0; } static void device_update(void *data) { struct device *device = data; struct impl *impl = device->impl; pw_log_debug("%p: device %p %08x %08x", impl, device, device->device->obj.avail, device->device->obj.changed); if (!SPA_FLAG_IS_SET(device->device->obj.avail, SM_DEVICE_CHANGE_MASK_INFO | SM_DEVICE_CHANGE_MASK_NODES | SM_DEVICE_CHANGE_MASK_PARAMS)) return; if (SPA_FLAG_IS_SET(device->device->obj.changed, SM_DEVICE_CHANGE_MASK_NODES | SM_DEVICE_CHANGE_MASK_PARAMS)) { activate_device(device); } } static const struct sm_object_events device_events = { SM_VERSION_OBJECT_EVENTS, .update = device_update }; static int handle_device(struct impl *impl, struct sm_object *obj) { const char *media_class, *str; struct device *device; if (obj->props == NULL) return 0; media_class = pw_properties_get(obj->props, PW_KEY_MEDIA_CLASS); str = pw_properties_get(obj->props, PW_KEY_DEVICE_API); pw_log_debug("%p: device "PW_KEY_MEDIA_CLASS":%s api:%s", impl, media_class, str); if (!spa_strstartswith(media_class, "Audio/")) return 0; if (!spa_streq(str, "alsa")) return 0; device = sm_object_add_data(obj, SESSION_KEY, sizeof(struct device)); device->impl = impl; device->id = obj->id; device->device = (struct sm_device*)obj; spa_list_init(&device->endpoint_list); pw_log_debug("%p: found alsa device %d media_class %s", impl, obj->id, media_class); sm_object_add_listener(obj, &device->listener, &device_events, device); return 0; } static void destroy_device(struct impl *impl, struct device *device) { deactivate_device(device); spa_hook_remove(&device->listener); sm_object_remove_data((struct sm_object*)device->device, SESSION_KEY); } static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; int res; if (spa_streq(object->type, PW_TYPE_INTERFACE_Device)) res = handle_device(impl, object); else res = 0; if (res < 0) { pw_log_warn("%p: can't handle global %d: %s", impl, object->id, spa_strerror(res)); } } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; if (spa_streq(object->type, PW_TYPE_INTERFACE_Device)) { struct device *device; if ((device = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_device(impl, device); } } static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->listener); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .destroy = session_destroy, }; int sm_alsa_endpoint_start(struct sm_media_session *session) { struct impl *impl; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; sm_media_session_add_listener(session, &impl->listener, &session_events, impl); return 0; } 07070100000060000081A40000000000000000000000016178A88C000014AE000000000000000000000000000000000000002400000000media-session-0.4.1/src/alsa-midi.c/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <limits.h> #include <sys/inotify.h> #include "config.h" #include <spa/utils/names.h> #include <spa/utils/result.h> #include <spa/node/keys.h> #include "pipewire/pipewire.h" #include "media-session.h" #define SND_PATH "/dev/snd" #define SEQ_NAME "seq" #define SND_SEQ_PATH SND_PATH"/"SEQ_NAME /** \page page_media_session_module_alsa_midi Media Session Module: ALSA MIDI */ #define NAME "alsa-midi" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic #define DEFAULT_NAME "Midi-Bridge" struct impl { struct sm_media_session *session; struct spa_hook listener; struct pw_properties *props; struct pw_proxy *proxy; struct spa_source *notify; }; static int do_create(struct impl *impl) { impl->proxy = sm_media_session_create_object(impl->session, "spa-node-factory", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, &impl->props->dict, 0); if (impl->proxy == NULL) return -errno; return 0; } static int check_access(struct impl *impl) { return access(SND_SEQ_PATH, R_OK|W_OK) >= 0; } static void stop_inotify(struct impl *impl) { struct pw_loop *main_loop = impl->session->loop; if (impl->notify != NULL) { pw_log_info("stop inotify"); pw_loop_destroy_source(main_loop, impl->notify); impl->notify = NULL; } } static void on_notify_events(void *data, int fd, uint32_t mask) { struct impl *impl = data; bool remove = false; struct { struct inotify_event e; char name[NAME_MAX+1]; } buf; while (true) { ssize_t len; const struct inotify_event *event; void *p, *e; len = read(fd, &buf, sizeof(buf)); if (len < 0 && errno != EAGAIN) break; if (len <= 0) break; e = SPA_PTROFF(&buf, len, void); for (p = &buf; p < e; p = SPA_PTROFF(p, sizeof(struct inotify_event) + event->len, void)) { event = (const struct inotify_event *) p; if ((event->mask & IN_ATTRIB)) { if (strncmp(event->name, SEQ_NAME, event->len) != 0) continue; if (impl->proxy == NULL && check_access(impl) && do_create(impl) >= 0) remove = true; } if ((event->mask & (IN_DELETE_SELF | IN_MOVE_SELF))) remove = true; } } if (remove) stop_inotify(impl); } static int start_inotify(struct impl *impl) { int notify_fd, res; struct pw_loop *main_loop = impl->session->loop; if ((notify_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK)) < 0) return -errno; res = inotify_add_watch(notify_fd, SND_PATH, IN_ATTRIB | IN_CLOSE_WRITE | IN_DELETE_SELF | IN_MOVE_SELF); if (res < 0) { res = -errno; close(notify_fd); pw_log_error("inotify_add_watch() '%s' failed: %s", SND_PATH, spa_strerror(res)); return res; } pw_log_info("start inotify"); impl->notify = pw_loop_add_io(main_loop, notify_fd, SPA_IO_IN | SPA_IO_ERR, true, on_notify_events, impl); return 0; } static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->listener); if (impl->proxy) pw_proxy_destroy(impl->proxy); stop_inotify(impl); pw_properties_free(impl->props); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .destroy = session_destroy, }; int sm_alsa_midi_start(struct sm_media_session *session) { struct impl *impl; int res; const char *name; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; if ((name = pw_properties_get(session->props, "alsa.seq.name")) == NULL) name = DEFAULT_NAME; impl->session = session; impl->props = pw_properties_new( SPA_KEY_FACTORY_NAME, SPA_NAME_API_ALSA_SEQ_BRIDGE, SPA_KEY_NODE_NAME, name, NULL); if (impl->props == NULL) { res = -errno; goto cleanup; } sm_media_session_add_listener(session, &impl->listener, &session_events, impl); if (check_access(impl)) { res = do_create(impl); } else { res = start_inotify(impl); } if (res < 0) goto cleanup_props; return 0; cleanup_props: pw_properties_free(impl->props); cleanup: free(impl); return res; } 07070100000061000081A40000000000000000000000016178A88C00007F92000000000000000000000000000000000000002700000000media-session-0.4.1/src/alsa-monitor.c/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include <regex.h> #include "config.h" #include <alsa/asoundlib.h> #include <spa/monitor/device.h> #include <spa/monitor/event.h> #include <spa/node/node.h> #include <spa/node/keys.h> #include <spa/utils/result.h> #include <spa/utils/hook.h> #include <spa/utils/names.h> #include <spa/utils/keys.h> #include <spa/utils/json.h> #include <spa/utils/string.h> #include <spa/param/props.h> #include <spa/pod/builder.h> #include <spa/pod/parser.h> #include <spa/debug/dict.h> #include <spa/debug/pod.h> #include <spa/support/dbus.h> #include <pipewire/pipewire.h> #include <pipewire/i18n.h> #include <pipewire/extensions/session-manager.h> #include "media-session.h" #include "reserve.h" /** \page page_media_session_module_alsa_monitor Media Session Module: ALSA Monitor * * This module monitors udev for ALSA devices and creates the required * PipeWire Device objects for each ALSA device. * * Devices advertised by udev are reserved using the [DBus ReserveDevice * API](http://git.0pointer.net/reserve.git/tree/reserve.txt) and exported as * \ref SPA_TYPE_INTERFACE_Device in the \ref pw_core. For each device, * objects of type \ref SPA_TYPE_INTERFACE_Node are then created as required. * * Additionally, extra configuration is applied as shown below. This * configuration is applied before the device is exported. * * ## Configuration * * This module loads the `alsa-monitor.conf` configuration file. The main * component in that file is the `rules = []` array that consists of multiple * dictionaries that `matches` a device and specifying `actions` to take. * * The following `actions` are supported: * - `update-props`: update properties on the matched object * * For example: * ``` * rules = [ * { * # Matches is an array of dictionaries. For a dictionary to match, **all** * # key/value matches must apply. For a match to be successful, **any** * # dictionary must apply. * matches = [ * { * # A regular expression is prefixed with ~ * device.name = "~alsa_card.*" * } * { * # standard string comparisons * device.name = "alsa_card.abcdef" * node.name = "alsa_input.12345" * } * { * # 'null' matches if the property is unset * some.random.property = "null" * } * ] * actions = { * update-props = { * api.alsa.use-acp = true * } * } * } * ] * ``` * * ## Module-specific properties: * * This modules supports the following entries in the `properties` dictionary: * - `alsa.reserve = false`: disable device reservation (default: enabled) * - `alsa.jack-device = true`: create a JACK device (default: disabled), see * the comment in the example configuration file. * * See the `alsa-monitor.conf` provided by your installation for details on * possible actions and matches. */ #define NAME "alsa-monitor" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic #define SESSION_CONF "alsa-monitor.conf" #define DEFAULT_JACK_SECONDS 1 struct node { struct impl *impl; enum pw_direction direction; struct device *device; struct spa_list link; uint32_t id; struct pw_properties *props; struct spa_node *node; struct sm_node *snode; unsigned int acquired:1; }; struct device { struct impl *impl; struct spa_list link; uint32_t id; uint32_t device_id; char *factory_name; struct rd_device *reserve; struct spa_hook sync_listener; int seq; int priority; int profile; int pending_profile; struct pw_properties *props; struct spa_handle *handle; struct spa_device *device; struct spa_hook device_listener; struct sm_device *sdevice; struct spa_hook listener; uint32_t n_acquired; unsigned int first:1; unsigned int appeared:1; unsigned int probed:1; unsigned int use_acp:1; struct spa_list node_list; }; struct impl { struct sm_media_session *session; struct spa_hook session_listener; struct pw_properties *conf; struct pw_properties *props; struct spa_handle *handle; struct spa_device *monitor; struct spa_hook listener; struct spa_list device_list; struct spa_source *jack_timeout; struct pw_proxy *jack_device; unsigned int reserve:1; }; #undef NAME #define NAME "alsa-monitor" static int probe_device(struct device *device); static int do_device_acquire(struct device *device); static struct node *alsa_find_node(struct device *device, uint32_t id, const char *name) { struct node *node; const char *str; spa_list_for_each(node, &device->node_list, link) { if (node->id == id) return node; if (name != NULL && (str = pw_properties_get(node->props, PW_KEY_NODE_NAME)) != NULL && spa_streq(name, str)) return node; } return NULL; } static void alsa_update_node(struct device *device, struct node *node, const struct spa_device_object_info *info) { pw_log_debug("update node %u", node->id); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); pw_properties_update(node->props, info->props); } static int node_acquire(void *data) { struct node *node = data; struct device *device = node->device; pw_log_debug("acquire %u", node->id); if (node->acquired) return 0; node->acquired = true; if (device && device->n_acquired++ == 0) return do_device_acquire(device); else return 0; } static int node_release(void *data) { struct node *node = data; struct device *device = node->device; pw_log_debug("release %u", node->id); if (!node->acquired) return 0; node->acquired = false; if (device && --device->n_acquired == 0 && device->reserve) rd_device_release(device->reserve); return 0; } static const struct sm_object_methods node_methods = { SM_VERSION_OBJECT_METHODS, .acquire = node_acquire, .release = node_release, }; static void update_icon_name(struct pw_properties *p, bool is_sink) { const char *s, *d = NULL, *bus; if ((s = pw_properties_get(p, PW_KEY_DEVICE_FORM_FACTOR))) { if (spa_streq(s, "microphone")) d = "audio-input-microphone"; else if (spa_streq(s, "webcam")) d = "camera-web"; else if (spa_streq(s, "computer")) d = "computer"; else if (spa_streq(s, "handset")) d = "phone"; else if (spa_streq(s, "portable")) d = "multimedia-player"; else if (spa_streq(s, "tv")) d = "video-display"; else if (spa_streq(s, "headset")) d = "audio-headset"; else if (spa_streq(s, "headphone")) d = "audio-headphones"; else if (spa_streq(s, "speaker")) d = "audio-speakers"; else if (spa_streq(s, "hands-free")) d = "audio-handsfree"; } if (!d) if ((s = pw_properties_get(p, PW_KEY_DEVICE_CLASS))) if (spa_streq(s, "modem")) d = "modem"; if (!d) { if (is_sink) d = "audio-card"; else d = "audio-input-microphone"; } if ((s = pw_properties_get(p, "device.profile.name")) != NULL) { if (strstr(s, "analog")) s = "-analog"; else if (strstr(s, "iec958")) s = "-iec958"; else if (strstr(s, "hdmi")) s = "-hdmi"; else s = NULL; } bus = pw_properties_get(p, PW_KEY_DEVICE_BUS); pw_properties_setf(p, PW_KEY_DEVICE_ICON_NAME, "%s%s%s%s", d, s ? s : "", bus ? "-" : "", bus ? bus : ""); } static struct node *alsa_create_node(struct device *device, uint32_t id, const struct spa_device_object_info *info) { struct node *node; struct impl *impl = device->impl; int res; const char *dev, *subdev, *stream, *profile, *profile_desc, *rules, *str; char tmp[1024]; int i, priority; pw_log_debug("new node %u", id); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); if (!spa_streq(info->type, SPA_TYPE_INTERFACE_Node)) { errno = EINVAL; return NULL; } node = calloc(1, sizeof(*node)); if (node == NULL) { res = -errno; goto exit; } node->props = pw_properties_new_dict(info->props); pw_properties_setf(node->props, PW_KEY_DEVICE_ID, "%d", device->device_id); pw_properties_set(node->props, PW_KEY_FACTORY_NAME, info->factory_name); if (!device->use_acp && pw_properties_get(node->props, PW_KEY_AUDIO_CHANNELS) == NULL) pw_properties_setf(node->props, PW_KEY_AUDIO_CHANNELS, "%d", SPA_AUDIO_MAX_CHANNELS); if ((dev = pw_properties_get(node->props, SPA_KEY_API_ALSA_PCM_DEVICE)) == NULL) if ((dev = pw_properties_get(node->props, "alsa.device")) == NULL) dev = "0"; if ((subdev = pw_properties_get(node->props, SPA_KEY_API_ALSA_PCM_SUBDEVICE)) == NULL) if ((subdev = pw_properties_get(node->props, "alsa.subdevice")) == NULL) subdev = "0"; if ((stream = pw_properties_get(node->props, SPA_KEY_API_ALSA_PCM_STREAM)) == NULL) stream = "unknown"; if ((profile = pw_properties_get(node->props, "device.profile.name")) == NULL) profile = "unknown"; profile_desc = pw_properties_get(node->props, "device.profile.description"); if (spa_streq(stream, "capture")) node->direction = PW_DIRECTION_OUTPUT; else node->direction = PW_DIRECTION_INPUT; if (device->first) { if (atol(dev) != 0) device->priority -= 256; device->first = false; } priority = device->priority; if (node->direction == PW_DIRECTION_OUTPUT) priority += 1000; priority -= atol(dev) * 16; priority -= atol(subdev); if (spa_strstartswith(profile, "analog-")) priority += 9; else if (spa_strstartswith(profile, "iec958-")) priority += 8; if (pw_properties_get(node->props, PW_KEY_PRIORITY_DRIVER) == NULL) { pw_properties_setf(node->props, PW_KEY_PRIORITY_DRIVER, "%d", priority); pw_properties_setf(node->props, PW_KEY_PRIORITY_SESSION, "%d", priority); } if (pw_properties_get(node->props, SPA_KEY_MEDIA_CLASS) == NULL) { if (node->direction == PW_DIRECTION_OUTPUT) pw_properties_setf(node->props, SPA_KEY_MEDIA_CLASS, "Audio/Source"); else pw_properties_setf(node->props, SPA_KEY_MEDIA_CLASS, "Audio/Sink"); } if (pw_properties_get(node->props, PW_KEY_NODE_NICK) == NULL) { const char *s; s = pw_properties_get(device->props, PW_KEY_DEVICE_NICK); if (s == NULL) s = pw_properties_get(device->props, SPA_KEY_API_ALSA_CARD_NAME); if (s == NULL) s = pw_properties_get(device->props, "alsa.card_name"); pw_properties_set(node->props, PW_KEY_NODE_NICK, sm_media_session_sanitize_description(tmp, sizeof(tmp), ' ', "%s", s)); } if (pw_properties_get(node->props, SPA_KEY_NODE_NAME) == NULL) { const char *devname, *d; if ((devname = pw_properties_get(device->props, SPA_KEY_DEVICE_NAME)) == NULL) devname = "unnamed-device"; if (spa_strstartswith(devname, "alsa_card.")) devname += 10; pw_properties_set(node->props, SPA_KEY_NODE_NAME, sm_media_session_sanitize_name(tmp, sizeof(tmp), '_', "%s.%s.%s", node->direction == PW_DIRECTION_OUTPUT ? "alsa_input" : "alsa_output", devname, profile)); for (i = 2; i <= 99; i++) { if ((d = pw_properties_get(node->props, PW_KEY_NODE_NAME)) == NULL) break; if (alsa_find_node(device, SPA_ID_INVALID, d) == NULL) break; pw_properties_set(node->props, SPA_KEY_NODE_NAME, sm_media_session_sanitize_name(tmp, sizeof(tmp), '_', "%s.%s.%s.%d", node->direction == PW_DIRECTION_OUTPUT ? "alsa_input" : "alsa_output", devname, profile, i)); } } if (pw_properties_get(node->props, PW_KEY_NODE_DESCRIPTION) == NULL) { const char *desc, *name = NULL; if ((desc = pw_properties_get(device->props, SPA_KEY_DEVICE_DESCRIPTION)) == NULL) desc = "unknown"; name = pw_properties_get(node->props, SPA_KEY_API_ALSA_PCM_NAME); if (name == NULL) name = pw_properties_get(node->props, SPA_KEY_API_ALSA_PCM_ID); if (name == NULL) name = dev; if (profile_desc != NULL) { pw_properties_set(node->props, PW_KEY_NODE_DESCRIPTION, sm_media_session_sanitize_description(tmp, sizeof(tmp), ' ', "%s %s", desc, profile_desc)); } else if (!spa_streq(subdev, "0")) { pw_properties_set(node->props, PW_KEY_NODE_DESCRIPTION, sm_media_session_sanitize_description(tmp, sizeof(tmp), ' ', "%s (%s %s)", desc, name, subdev)); } else if (!spa_streq(dev, "0")) { pw_properties_set(node->props, PW_KEY_NODE_DESCRIPTION, sm_media_session_sanitize_description(tmp, sizeof(tmp), ' ', "%s (%s)", desc, name)); } else { pw_properties_set(node->props, PW_KEY_NODE_DESCRIPTION, sm_media_session_sanitize_description(tmp, sizeof(tmp), ' ', "%s", desc)); } } if (pw_properties_get(node->props, PW_KEY_DEVICE_ICON_NAME) == NULL) update_icon_name(node->props, node->direction == PW_DIRECTION_INPUT); if ((str = pw_properties_get(device->props, PW_KEY_DEVICE_BUS)) != NULL) pw_properties_set(node->props, PW_KEY_DEVICE_BUS, str); if ((str = pw_properties_get(device->props, PW_KEY_DEVICE_BUS_PATH)) != NULL) pw_properties_set(node->props, PW_KEY_DEVICE_BUS_PATH, str); if ((str = pw_properties_get(device->props, PW_KEY_DEVICE_FORM_FACTOR)) != NULL) pw_properties_set(node->props, PW_KEY_DEVICE_FORM_FACTOR, str); node->impl = impl; node->device = device; node->id = id; if ((rules = pw_properties_get(impl->conf, "rules")) != NULL) sm_media_session_match_rules(rules, strlen(rules), node->props); node->snode = sm_media_session_create_node(impl->session, "adapter", &node->props->dict); if (node->snode == NULL) { res = -errno; goto clean_node; } node->snode->obj.methods = SPA_CALLBACKS_INIT(&node_methods, node); spa_list_append(&device->node_list, &node->link); return node; clean_node: pw_properties_free(node->props); free(node); exit: errno = -res; return NULL; } static void alsa_remove_node(struct device *device, struct node *node) { pw_log_debug("remove node %u", node->id); spa_list_remove(&node->link); sm_object_destroy(&node->snode->obj); pw_properties_free(node->props); free(node); } static void alsa_device_info(void *data, const struct spa_device_info *info) { struct device *device = data; if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); pw_properties_update(device->props, info->props); } static void alsa_device_object_info(void *data, uint32_t id, const struct spa_device_object_info *info) { struct device *device = data; struct node *node; node = alsa_find_node(device, id, NULL); if (info == NULL) { if (node == NULL) { pw_log_warn("device %p: unknown node %u", device, id); return; } alsa_remove_node(device, node); } else if (node == NULL) { alsa_create_node(device, id, info); } else { alsa_update_node(device, node, info); } } static void alsa_device_event(void *data, const struct spa_event *event) { struct device *device = data; struct node *node; uint32_t id, type; struct spa_pod *props = NULL; if (spa_pod_parse_object(&event->pod, SPA_TYPE_EVENT_Device, &type, SPA_EVENT_DEVICE_Object, SPA_POD_Int(&id), SPA_EVENT_DEVICE_Props, SPA_POD_OPT_Pod(&props)) < 0) return; if ((node = alsa_find_node(device, id, NULL)) == NULL) return; switch (type) { case SPA_DEVICE_EVENT_ObjectConfig: if (props && !node->snode->obj.destroyed) pw_node_set_param((struct pw_node*)node->snode->obj.proxy, SPA_PARAM_Props, 0, props); break; default: break; } } static const struct spa_device_events alsa_device_events = { SPA_VERSION_DEVICE_EVENTS, .info = alsa_device_info, .object_info = alsa_device_object_info, .event = alsa_device_event, }; static struct device *alsa_find_device(struct impl *impl, uint32_t id, const char *name) { struct device *device; const char *str; spa_list_for_each(device, &impl->device_list, link) { if (device->id == id) return device; if (name != NULL && (str = pw_properties_get(device->props, PW_KEY_DEVICE_NAME)) != NULL && spa_streq(str, name)) return device; } return NULL; } static void alsa_update_device(struct impl *impl, struct device *device, const struct spa_device_object_info *info) { pw_log_debug("update device %u", device->id); if (info->change_mask & SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS) { if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); pw_properties_update(device->props, info->props); } } static int update_device_props(struct device *device) { struct pw_properties *p = device->props; const char *s, *d; char temp[32], tmp[1024]; int i; s = pw_properties_get(p, SPA_KEY_DEVICE_NAME); if (s == NULL) s = pw_properties_get(p, SPA_KEY_DEVICE_BUS_ID); if (s == NULL) s = pw_properties_get(p, SPA_KEY_DEVICE_BUS_PATH); if (s == NULL) { snprintf(temp, sizeof(temp), "%d", device->id); s = temp; } pw_properties_set(p, PW_KEY_DEVICE_NAME, sm_media_session_sanitize_name(tmp, sizeof(tmp), '_', "alsa_card.%s", s)); for (i = 2; i <= 99; i++) { if ((d = pw_properties_get(p, PW_KEY_DEVICE_NAME)) == NULL) break; if (alsa_find_device(device->impl, SPA_ID_INVALID, d) == NULL) break; pw_properties_set(p, PW_KEY_DEVICE_NAME, sm_media_session_sanitize_name(tmp, sizeof(tmp), '_', "alsa_card.%s.%d", s, i)); } if (i == 99) return -EEXIST; if (pw_properties_get(p, PW_KEY_DEVICE_DESCRIPTION) == NULL) { d = NULL; if ((s = pw_properties_get(p, PW_KEY_DEVICE_FORM_FACTOR))) if (spa_streq(s, "internal")) d = _("Built-in Audio"); if (!d) if ((s = pw_properties_get(p, PW_KEY_DEVICE_CLASS))) if (spa_streq(s, "modem")) d = _("Modem"); if (!d) d = pw_properties_get(p, PW_KEY_DEVICE_PRODUCT_NAME); if (!d) d = pw_properties_get(p, SPA_KEY_API_ALSA_CARD_NAME); if (!d) d = pw_properties_get(p, "alsa.card_name"); if (!d) d = _("Unknown device"); pw_properties_set(p, PW_KEY_DEVICE_DESCRIPTION, d); } if (pw_properties_get(p, PW_KEY_DEVICE_NICK) == NULL) { s = pw_properties_get(p, SPA_KEY_API_ALSA_CARD_NAME); if (s != NULL) pw_properties_set(p, PW_KEY_DEVICE_NICK, s); } if (pw_properties_get(p, PW_KEY_DEVICE_ICON_NAME) == NULL) update_icon_name(device->props, true); return 1; } static void set_profile(struct device *device, int index) { char buf[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); if (device->use_acp) return; pw_log_debug("%p: set profile %d id:%d", device, index, device->device_id); if (device->device_id != 0) { device->profile = index; spa_device_set_param(device->device, SPA_PARAM_Profile, 0, spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile, SPA_PARAM_PROFILE_index, SPA_POD_Int(index))); } } static void set_jack_profile(struct impl *impl, int index) { char buf[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); if (impl->jack_device == NULL) return; pw_device_set_param((struct pw_device*)impl->jack_device, SPA_PARAM_Profile, 0, spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile, SPA_PARAM_PROFILE_index, SPA_POD_Int(index))); } static void remove_jack_timeout(struct impl *impl) { struct pw_loop *main_loop = impl->session->loop; if (impl->jack_timeout) { pw_loop_destroy_source(main_loop, impl->jack_timeout); impl->jack_timeout = NULL; } } static void jack_timeout(void *data, uint64_t expirations) { struct impl *impl = data; remove_jack_timeout(impl); set_jack_profile(impl, 1); } static void add_jack_timeout(struct impl *impl) { struct timespec value; struct pw_loop *main_loop = impl->session->loop; if (impl->jack_timeout == NULL) impl->jack_timeout = pw_loop_add_timer(main_loop, jack_timeout, impl); value.tv_sec = DEFAULT_JACK_SECONDS; value.tv_nsec = 0; pw_loop_update_timer(main_loop, impl->jack_timeout, &value, NULL, false); } static void do_reserve_acquired(struct device *device) { if (!device->probed) probe_device(device); if (device->n_acquired == 0 && device->reserve) rd_device_release(device->reserve); } static void reserve_acquired(void *data, struct rd_device *d) { struct device *device = data; pw_log_info("%p: reserve acquired %d", device, device->n_acquired); do_reserve_acquired(device); } static void complete_release(struct device *device) { if (device->reserve) rd_device_complete_release(device->reserve, true); } static void sync_complete_done(void *data, int seq) { struct device *device = data; pw_log_debug("%d %d", device->seq, seq); if (seq != device->seq) return; spa_hook_remove(&device->sync_listener); device->seq = 0; complete_release(device); } static void sync_destroy(void *data) { struct device *device = data; if (device->seq != 0) sync_complete_done(data, device->seq); } static const struct pw_proxy_events sync_complete_release = { PW_VERSION_PROXY_EVENTS, .destroy = sync_destroy, .done = sync_complete_done }; static void reserve_release(void *data, struct rd_device *d, int forced) { struct device *device = data; pw_log_info("%p: reserve release", device); if (device->sdevice == NULL || device->sdevice->obj.proxy == NULL) { complete_release(device); return; } set_profile(device, 0); if (device->seq == 0) pw_proxy_add_listener(device->sdevice->obj.proxy, &device->sync_listener, &sync_complete_release, device); device->seq = pw_proxy_sync(device->sdevice->obj.proxy, 0); } static void reserve_busy(void *data, struct rd_device *d, const char *name, int32_t prio) { struct device *device = data; struct impl *impl = device->impl; pw_log_info("%p: reserve busy %s", device, name); if (device->sdevice == NULL) return ; device->sdevice->locked = true; if (spa_streq(name, "jack")) { add_jack_timeout(impl); } else { remove_jack_timeout(impl); } } static void reserve_available(void *data, struct rd_device *d, const char *name) { struct device *device = data; struct impl *impl = device->impl; pw_log_info("%p: reserve available %s", device, name); if (device->sdevice == NULL) return ; device->sdevice->locked = false; remove_jack_timeout(impl); if (spa_streq(name, "jack")) { set_jack_profile(impl, 0); } } static const struct rd_device_callbacks reserve_callbacks = { .acquired = reserve_acquired, .release = reserve_release, .busy = reserve_busy, .available = reserve_available, }; static int do_device_acquire(struct device *device) { struct impl *impl = device->impl; struct sm_media_session *session = impl->session; int res; if (!impl->reserve) goto done; if (device->reserve == NULL) { const char *reserve; const char *path = pw_properties_get(device->props, SPA_KEY_API_ALSA_PATH); DBusConnection *conn = NULL; if (session->dbus_connection) conn = spa_dbus_connection_get(session->dbus_connection); if (conn == NULL) { pw_log_warn("no dbus connection, device reservation disabled"); goto done; } pw_log_debug("got dbus connection %p", conn); reserve = pw_properties_get(device->props, "api.dbus.ReserveDevice1"); if (reserve == NULL) goto done; device->reserve = rd_device_new(conn, reserve, "PipeWire", -10, &reserve_callbacks, device); if (device->reserve == NULL) { pw_log_warn("can't create device reserve for %s: %m", reserve); goto done; } else if (path) { rd_device_set_application_device_name(device->reserve, path); } else { pw_log_warn("empty reserve device path for %s", reserve); } } res = rd_device_acquire(device->reserve); if (res < 0 && res != -EBUSY) { pw_log_warn("device reserve failed, disabling: %s", spa_strerror(res)); goto done; } return res; done: do_reserve_acquired(device); if (device->reserve != NULL) { rd_device_destroy(device->reserve); device->reserve = NULL; } return 0; } static void device_destroy(void *data) { struct device *device = data; struct node *node; pw_log_debug("device %p destroy", device); spa_list_remove(&device->link); spa_list_consume(node, &device->node_list, link) alsa_remove_node(device, node); if (device->appeared) spa_hook_remove(&device->device_listener); if (device->seq != 0) spa_hook_remove(&device->sync_listener); if (device->reserve) rd_device_destroy(device->reserve); } static void device_free(void *data) { struct device *device = data; pw_log_debug("device %p free", device); spa_hook_remove(&device->listener); free(device->factory_name); pw_unload_spa_handle(device->handle); pw_properties_free(device->props); sm_object_discard(&device->sdevice->obj); free(device); } static void device_update(void *data) { struct device *device = data; pw_log_debug("device %p appeared %d %d", device, device->appeared, device->profile); if (!device->appeared) { device->device_id = device->sdevice->obj.id; device->appeared = true; spa_device_add_listener(device->device, &device->device_listener, &alsa_device_events, device); sm_object_sync_update(&device->sdevice->obj); } if (device->pending_profile != device->profile && !device->sdevice->locked) set_profile(device, device->pending_profile); } static const struct sm_object_events device_events = { SM_VERSION_OBJECT_EVENTS, .destroy = device_destroy, .free = device_free, .update = device_update, }; static int probe_device(struct device *device) { struct impl *impl = device->impl; struct pw_context *context = impl->session->context; struct spa_handle *handle; void *iface; int res; if (device->probed) return 0; handle = pw_context_load_spa_handle(context, device->factory_name, &device->props->dict); if (handle == NULL) { res = -errno; pw_log_error("can't make factory instance: %m"); goto exit; } if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Device, &iface)) < 0) { pw_log_error("can't get %s interface: %s", SPA_TYPE_INTERFACE_Device, spa_strerror(res)); goto unload_handle; } device->handle = handle; device->device = iface; device->sdevice = sm_media_session_export_device(impl->session, &device->props->dict, device->device); if (device->sdevice == NULL) { res = -errno; goto unload_handle; } sm_object_add_listener(&device->sdevice->obj, &device->listener, &device_events, device); device->probed = true; return 0; unload_handle: pw_unload_spa_handle(handle); exit: return res; } static struct device *alsa_create_device(struct impl *impl, uint32_t id, const struct spa_device_object_info *info) { struct device *device; int res; const char *card, *rules; pw_log_debug("new device %u", id); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); if (!spa_streq(info->type, SPA_TYPE_INTERFACE_Device)) { errno = EINVAL; return NULL; } device = calloc(1, sizeof(*device)); if (device == NULL) { res = -errno; goto exit; } device->impl = impl; device->id = id; device->props = pw_properties_new_dict(info->props); device->priority = 1000; device->first = true; spa_list_init(&device->node_list); update_device_props(device); device->pending_profile = 1; spa_list_append(&impl->device_list, &device->link); if ((rules = pw_properties_get(impl->conf, "rules")) != NULL) sm_media_session_match_rules(rules, strlen(rules), device->props); device->use_acp = pw_properties_get_bool(device->props, "api.alsa.use-acp", true); if (device->use_acp) device->factory_name = strdup(SPA_NAME_API_ALSA_ACP_DEVICE); else device->factory_name = strdup(info->factory_name); if ((card = spa_dict_lookup(info->props, SPA_KEY_API_ALSA_CARD)) != NULL) { device->priority -= atol(card) * 64; pw_properties_setf(device->props, "api.dbus.ReserveDevice1", "Audio%s", card); } do_device_acquire(device); return device; exit: errno = -res; return NULL; } static void alsa_remove_device(struct impl *impl, struct device *device) { pw_log_debug("%p: remove device %u", device, device->id); if (device->sdevice) sm_object_destroy(&device->sdevice->obj); } static void alsa_udev_object_info(void *data, uint32_t id, const struct spa_device_object_info *info) { struct impl *impl = data; struct device *device; device = alsa_find_device(impl, id, NULL); if (info == NULL) { if (device == NULL) return; alsa_remove_device(impl, device); } else if (device == NULL) { if ((device = alsa_create_device(impl, id, info)) == NULL) return; } else { alsa_update_device(impl, device, info); } sm_media_session_schedule_rescan(impl->session); } static const struct spa_device_events alsa_udev_events = { SPA_VERSION_DEVICE_EVENTS, .object_info = alsa_udev_object_info, }; static int alsa_start_jack_device(struct impl *impl) { struct pw_properties *props; int res = 0; props = pw_properties_new( SPA_KEY_FACTORY_NAME, SPA_NAME_API_JACK_DEVICE, SPA_KEY_NODE_NAME, "JACK-Device", NULL); impl->jack_device = sm_media_session_create_object(impl->session, "spa-device-factory", PW_TYPE_INTERFACE_Device, PW_VERSION_DEVICE, &props->dict, 0); if (impl->jack_device == NULL) { pw_log_error("can't create JACK Device: %m"); res = -errno; } pw_properties_free(props); return res; } static void session_destroy(void *data) { struct impl *impl = data; remove_jack_timeout(impl); spa_hook_remove(&impl->session_listener); spa_hook_remove(&impl->listener); if (impl->jack_device) pw_proxy_destroy(impl->jack_device); pw_unload_spa_handle(impl->handle); pw_properties_free(impl->props); pw_properties_free(impl->conf); free(impl); } static void session_dbus_disconnected(void *data) { struct device *d; struct impl *impl = data; spa_list_for_each(d, &impl->device_list, link) { if (d->reserve != NULL) rd_device_destroy(d->reserve); d->reserve = NULL; } } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .destroy = session_destroy, .dbus_disconnected = session_dbus_disconnected, }; int sm_alsa_monitor_start(struct sm_media_session *session) { struct pw_context *context = session->context; struct impl *impl; void *iface; int res; const char *str; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; impl->conf = pw_properties_new(NULL, NULL); if (impl->conf == NULL) { res = -errno; goto out_free; } if ((res = sm_media_session_load_conf(impl->session, SESSION_CONF, impl->conf)) < 0) pw_log_info("can't load "SESSION_CONF" config: %s", spa_strerror(res)); if ((impl->props = pw_properties_new(NULL, NULL)) == NULL) { res = -errno; goto out_free; } if ((str = pw_properties_get(impl->conf, "properties")) != NULL) pw_properties_update_string(impl->props, str, strlen(str)); impl->reserve = pw_properties_get_bool(impl->props, "alsa.reserve", true); impl->handle = pw_context_load_spa_handle(context, SPA_NAME_API_ALSA_ENUM_UDEV, NULL); if (impl->handle == NULL) { res = -errno; pw_log_info("can't load %s: %m", SPA_NAME_API_ALSA_ENUM_UDEV); goto out_free; } if ((res = spa_handle_get_interface(impl->handle, SPA_TYPE_INTERFACE_Device, &iface)) < 0) { pw_log_error("can't get udev Device interface: %d", res); goto out_free; } impl->monitor = iface; spa_list_init(&impl->device_list); spa_device_add_listener(impl->monitor, &impl->listener, &alsa_udev_events, impl); if (pw_properties_get_bool(impl->props, "alsa.jack-device", true)) { if ((res = alsa_start_jack_device(impl)) < 0) goto out_free; } sm_media_session_add_listener(session, &impl->session_listener, &session_events, impl); return 0; out_free: if (impl->handle) pw_unload_spa_handle(impl->handle); pw_properties_free(impl->conf); pw_properties_free(impl->props); free(impl); return res; } 07070100000062000081A40000000000000000000000016178A88C000007CE000000000000000000000000000000000000002600000000media-session-0.4.1/src/alsa-no-dsp.c/* PipeWire * * Copyright © 2021 Collabora Ltd. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include "config.h" #include "pipewire/pipewire.h" #include "pipewire/extensions/metadata.h" #include "media-session.h" /** \page page_media_session_module_no_dsp Media Session Module: No DSP * * Instruct \ref page_media_session_module_policy_node to not configure audio * adapter nodes in DSP mode. Device nodes will always be configured in * passthrough mode. If a client node wants to be linked with a device node * that has a different format, then the policy will configure the client node * in convert mode so that both nodes have the same format. * * This is done by just setting a session property flag, and policy-node does the rest. */ #define KEY_NAME "policy-node.alsa-no-dsp" int sm_alsa_no_dsp_start(struct sm_media_session *session) { pw_properties_set(session->props, KEY_NAME, "true"); return 0; } 07070100000063000081A40000000000000000000000016178A88C00004C1A000000000000000000000000000000000000002B00000000media-session-0.4.1/src/bluez-autoswitch.c/* PipeWire * * Copyright © 2021 Pauli Virtanen * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include "config.h" #include <spa/utils/hook.h> #include <spa/utils/result.h> #include <spa/utils/json.h> #include <spa/utils/string.h> #include <spa/pod/builder.h> #include <spa/pod/parser.h> #include "pipewire/pipewire.h" #include "pipewire/extensions/metadata.h" #include "media-session.h" /** \page page_media_session_module_bluez_autoswitch Media Session Module: BlueZ Auto-Switch * * Switch profiles of Bluetooth devices trying to enable an input route, * if input streams are active while default output is directed to * the device. Profiles are restored once there are no active input streams. * * Not all input streams are considered, with behavior depending on * configuration file settings. */ #define NAME "bluez-autoswitch" #define SESSION_KEY "bluez-autoswitch" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic #define RESTORE_DELAY_SEC 3 #define DEFAULT_AUDIO_SINK_KEY "default.audio.sink" struct impl { struct sm_media_session *session; struct spa_hook listener; struct spa_hook meta_listener; unsigned int record_count; unsigned int communication_count; struct pw_context *context; struct spa_source *restore_timeout; char *default_sink; struct pw_properties *properties; bool switched; }; struct node { struct sm_node *obj; struct impl *impl; struct spa_hook listener; unsigned char active:1; unsigned char communication:1; unsigned char was_active:1; }; struct find_data { const char *type; const char *name; uint32_t id; struct sm_object *obj; }; static int find_check(void *data, struct sm_object *object) { struct find_data *d = data; if (!spa_streq(object->type, d->type) || !object->props) return 0; if (d->id != SPA_ID_INVALID && d->id == object->id) { d->obj = object; return 1; } if (d->name != NULL && spa_streq(pw_properties_get(object->props, PW_KEY_NODE_NAME), d->name)) { d->obj = object; return 1; } return 0; } static struct sm_object *find_by_name(struct impl *impl, const char *type, const char *name) { struct find_data d = { type, name, SPA_ID_INVALID, NULL }; if (name != NULL) sm_media_session_for_each_object(impl->session, find_check, &d); return d.obj; } static struct sm_object *find_by_id(struct impl *impl, const char *type, uint32_t id) { struct find_data d = { type, NULL, id, NULL }; if (id != SPA_ID_INVALID) sm_media_session_for_each_object(impl->session, find_check, &d); return d.obj; } static struct sm_device *find_default_output_device(struct impl *impl) { struct sm_object *obj; uint32_t device_id; if ((obj = find_by_name(impl, PW_TYPE_INTERFACE_Node, impl->default_sink)) == NULL || !obj->props) return NULL; if (pw_properties_fetch_uint32(obj->props, PW_KEY_DEVICE_ID, &device_id) < 0) return NULL; if ((obj = find_by_id(impl, PW_TYPE_INTERFACE_Device, device_id)) == NULL) return NULL; if (!spa_streq(obj->type, PW_TYPE_INTERFACE_Device) || !obj->props) return NULL; return SPA_CONTAINER_OF(obj, struct sm_device, obj); } static int find_profile(struct sm_device *dev, int32_t index, const char *name, int32_t *out_index, const char **out_name, int32_t *out_priority) { struct sm_param *p; spa_list_for_each(p, &dev->param_list, link) { int32_t idx; int32_t prio = 0; const char *str; if (p->id != SPA_PARAM_EnumProfile || !p->param) continue; if (spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_ParamProfile, NULL, SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx), SPA_PARAM_PROFILE_name, SPA_POD_String(&str), SPA_PARAM_PROFILE_priority, SPA_POD_OPT_Int(&prio)) < 0) continue; if ((index < 0 || idx == index) && (name == NULL || spa_streq(str, name))) { if (out_index) *out_index = idx; if (out_name) *out_name = str; if (out_priority) *out_priority = prio; return 0; } } return -ENOENT; } static int set_profile(struct sm_device *dev, const char *profile_name) { char buf[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); int32_t index = -1; int ret; if (!profile_name) return -EINVAL; if (!dev->obj.proxy) return -ENOENT; if ((ret = find_profile(dev, -1, profile_name, &index, NULL, NULL)) < 0) return ret; pw_log_info("switching device %d to profile %s", dev->obj.id, profile_name); return pw_device_set_param((struct pw_device *)dev->obj.proxy, SPA_PARAM_Profile, 0, spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile, SPA_PARAM_PROFILE_index, SPA_POD_Int(index))); } const char *get_saved_profile(struct impl *impl, const char *dev_name) { char saved_profile_key[512]; spa_scnprintf(saved_profile_key, sizeof(saved_profile_key), "%s:profile", dev_name); return pw_properties_get(impl->properties, saved_profile_key); } void set_saved_profile(struct impl *impl, const char *dev_name, const char *profile_name) { char saved_profile_key[512]; spa_scnprintf(saved_profile_key, sizeof(saved_profile_key), "%s:profile", dev_name); pw_properties_set(impl->properties, saved_profile_key, profile_name); } bool get_pending_save(struct impl *impl, const char *dev_name) { char saved_profile_key[512]; spa_scnprintf(saved_profile_key, sizeof(saved_profile_key), "%s:pending-save", dev_name); return pw_properties_get_bool(impl->properties, saved_profile_key, false); } void set_pending_save(struct impl *impl, const char *dev_name, bool pending) { char saved_profile_key[512]; spa_scnprintf(saved_profile_key, sizeof(saved_profile_key), "%s:pending-save", dev_name); pw_properties_set(impl->properties, saved_profile_key, pending ? "true" : NULL); } const char *get_saved_headset_profile(struct impl *impl, const char *dev_name) { char saved_profile_key[512]; spa_scnprintf(saved_profile_key, sizeof(saved_profile_key), "%s:headset-profile", dev_name); return pw_properties_get(impl->properties, saved_profile_key); } void set_saved_headset_profile(struct impl *impl, const char *dev_name, const char *profile_name) { char saved_profile_key[512]; spa_scnprintf(saved_profile_key, sizeof(saved_profile_key), "%s:headset-profile", dev_name); pw_properties_set(impl->properties, saved_profile_key, profile_name); } static int do_restore_profile(void *data, struct sm_object *obj) { struct impl *impl = data; struct sm_device *dev; const char *dev_name; const char *profile_name; struct sm_param *p; /* Find old profile and restore it */ if (!spa_streq(obj->type, PW_TYPE_INTERFACE_Device) || !obj->props) goto next; if ((dev_name = pw_properties_get(obj->props, PW_KEY_DEVICE_NAME)) == NULL) goto next; if ((profile_name = get_saved_profile(impl, dev_name)) == NULL) goto next; dev = SPA_CONTAINER_OF(obj, struct sm_device, obj); /* Save user-selected headset profile */ if (get_pending_save(impl, dev_name)) { spa_list_for_each(p, &dev->param_list, link) { const char *name; if (p->id != SPA_PARAM_Profile || !p->param) continue; if (spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_ParamProfile, NULL, SPA_PARAM_PROFILE_name, SPA_POD_String(&name)) < 0) continue; set_saved_headset_profile(impl, dev_name, name); set_pending_save(impl, dev_name, false); break; } } /* Restore previous profile */ set_profile(dev, profile_name); set_saved_profile(impl, dev_name, NULL); next: return 0; } static void remove_restore_timeout(struct impl *impl) { struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); if (impl->restore_timeout) { pw_loop_destroy_source(main_loop, impl->restore_timeout); impl->restore_timeout = NULL; } } static void restore_timeout(void *data, uint64_t expirations) { struct impl *impl = data; int res; remove_restore_timeout(impl); /* * Switching profiles may make applications remove existing input streams * and create new ones. To avoid getting into a rapidly spinning loop, * restoring profiles has to be done with a timeout. */ /* Restore previous profiles to devices */ sm_media_session_for_each_object(impl->session, do_restore_profile, impl); if ((res = sm_media_session_save_state(impl->session, SESSION_KEY, impl->properties)) < 0) pw_log_error("can't save "SESSION_KEY" state: %s", spa_strerror(res)); impl->switched = false; } static void add_restore_timeout(struct impl *impl) { struct timespec value; struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); if (!impl->switched) return; if (impl->restore_timeout == NULL) impl->restore_timeout = pw_loop_add_timer(main_loop, restore_timeout, impl); value.tv_sec = RESTORE_DELAY_SEC; value.tv_nsec = 0; pw_loop_update_timer(main_loop, impl->restore_timeout, &value, NULL, false); } static void switch_profile_if_needed(struct impl *impl) { struct sm_device *dev; struct sm_param *p; int headset_profile_priority = -1; const char *current_profile_name = NULL; const char *headset_profile_name = NULL; enum spa_direction direction; const char *dev_name; const char *saved_headset_profile = NULL; const char *str; int res; if (impl->record_count == 0) goto inactive; pw_log_debug("considering switching device profiles"); if ((dev = find_default_output_device(impl)) == NULL) goto inactive; /* Handle only bluez devices */ if (!spa_streq(pw_properties_get(dev->obj.props, PW_KEY_DEVICE_API), "bluez5")) goto inactive; if ((dev_name = pw_properties_get(dev->obj.props, PW_KEY_DEVICE_NAME)) == NULL) goto inactive; /* Check autoswitch setting (default: role) */ if ((str = pw_properties_get(dev->obj.props, "bluez5.autoswitch-profile")) == NULL) str = "role"; if (spa_atob(str)) { /* ok */ } else if (spa_streq(str, "role")) { if (impl->communication_count == 0) goto inactive; } else { goto inactive; } /* BT microphone is wanted */ remove_restore_timeout(impl); if (get_saved_profile(impl, dev_name)) { /* We already switched this device */ return; } /* Find saved headset profile, if any */ saved_headset_profile = get_saved_headset_profile(impl, dev_name); /* Find current profile, and highest-priority profile with input route */ spa_list_for_each(p, &dev->param_list, link) { const char *name; int32_t idx; struct spa_pod *profiles = NULL; if (!p->param) continue; switch (p->id) { case SPA_PARAM_Route: case SPA_PARAM_EnumRoute: if (spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_ParamRoute, NULL, SPA_PARAM_ROUTE_direction, SPA_POD_Id(&direction), SPA_PARAM_ROUTE_profiles, SPA_POD_OPT_Pod(&profiles)) < 0) continue; if (direction != SPA_DIRECTION_INPUT) continue; if (p->id == SPA_PARAM_Route) { /* There's already an input route, no need to switch */ return; } else if (profiles) { /* Take highest-priority (or first) profile in the input route */ uint32_t *vals, n_vals, n; vals = spa_pod_get_array(profiles, &n_vals); if (vals == NULL) continue; for (n = 0; n < n_vals; ++n) { int32_t i = vals[n]; int32_t prio = -1; const char *name = NULL; if (find_profile(dev, i, NULL, NULL, &name, &prio) < 0) continue; if (headset_profile_priority < prio) { headset_profile_priority = prio; headset_profile_name = name; } } } break; case SPA_PARAM_Profile: case SPA_PARAM_EnumProfile: if (spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_ParamProfile, NULL, SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx), SPA_PARAM_PROFILE_name, SPA_POD_String(&name)) < 0) continue; if (p->id == SPA_PARAM_Profile) { current_profile_name = name; } else if (spa_streq(name, saved_headset_profile)) { /* Saved headset profile takes priority */ headset_profile_priority = INT32_MAX; headset_profile_name = name; } break; } } if (set_profile(dev, headset_profile_name) < 0) return; set_saved_profile(impl, dev_name, current_profile_name); set_pending_save(impl, dev_name, true); if ((res = sm_media_session_save_state(impl->session, SESSION_KEY, impl->properties)) < 0) pw_log_error("can't save "SESSION_KEY" state: %s", spa_strerror(res)); impl->switched = true; return; inactive: add_restore_timeout(impl); return; } static void change_node_state(struct node *node, bool active, bool communication) { bool need_switch = false; struct impl *impl = node->impl; node->was_active = node->was_active || active; if (node->active != active) { impl->record_count += active ? 1 : -1; node->active = active; need_switch = true; } if (node->communication != communication) { impl->communication_count += communication ? 1 : -1; node->communication = communication; need_switch = true; } if (need_switch) switch_profile_if_needed(impl); } static void check_node(struct node *node) { const char *str; bool communication = false; if (!node->obj || !node->obj->obj.props || !node->obj->info || !node->obj->info->props) goto inactive; if (!spa_streq(pw_properties_get(node->obj->obj.props, PW_KEY_MEDIA_CLASS), "Stream/Input/Audio")) goto inactive; if ((str = spa_dict_lookup(node->obj->info->props, PW_KEY_NODE_AUTOCONNECT)) == NULL || !spa_atob(str)) goto inactive; if ((str = spa_dict_lookup(node->obj->info->props, PW_KEY_STREAM_MONITOR)) != NULL && spa_atob(str)) goto inactive; /* * XXX: This is not fully the right thing to do --- the node may be * XXX: idle/suspended also because it's linked to a source that is not * XXX: generating data. However, this seems the closest approximation * XXX: to Pulse corked stream status. */ if (node->was_active && (node->obj->info->state == PW_NODE_STATE_SUSPENDED || node->obj->info->state == PW_NODE_STATE_IDLE || node->obj->info->state == PW_NODE_STATE_ERROR)) goto inactive; if (spa_streq(pw_properties_get(node->obj->obj.props, PW_KEY_MEDIA_ROLE), "Communication")) communication = true; change_node_state(node, true, communication); return; inactive: change_node_state(node, false, false); } static void object_update(void *data) { struct node *node = data; if (node->obj->obj.avail & (SM_NODE_CHANGE_MASK_PARAMS | SM_NODE_CHANGE_MASK_INFO)) check_node(node); } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; struct node *node; if (spa_streq(object->type, PW_TYPE_INTERFACE_Device) && object->props) { const char *str; if ((str = pw_properties_get(object->props, PW_KEY_DEVICE_NAME)) != NULL) set_pending_save(impl, str, false); impl->switched = true; add_restore_timeout(impl); return; } if (!spa_streq(object->type, PW_TYPE_INTERFACE_Node) || !object->props) return; if (!spa_streq(pw_properties_get(object->props, PW_KEY_MEDIA_CLASS), "Stream/Input/Audio")) return; pw_log_debug("input stream %d added", object->id); node = sm_object_add_data(object, SESSION_KEY, sizeof(struct node)); if (!node->obj) { node->obj = (struct sm_node *)object; node->impl = impl; sm_object_add_listener(&node->obj->obj, &node->listener, &object_events, node); } check_node(node); } static void session_remove(void *data, struct sm_object *object) { struct node *node; if (!spa_streq(object->type, PW_TYPE_INTERFACE_Node)) return; if ((node = sm_object_get_data(object, SESSION_KEY)) == NULL) return; change_node_state(node, false, false); if (node->obj) { pw_log_debug("input stream %d removed", object->id); spa_hook_remove(&node->listener); node->obj = NULL; } } static void session_destroy(void *data) { struct impl *impl = data; remove_restore_timeout(impl); spa_hook_remove(&impl->listener); if (impl->session->metadata) spa_hook_remove(&impl->meta_listener); pw_properties_free(impl->properties); free(impl->default_sink); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .destroy = session_destroy, }; static int json_object_find(const char *obj, const char *key, char *value, size_t len) { struct spa_json it[2]; const char *v; char k[128]; spa_json_init(&it[0], obj, strlen(obj)); if (spa_json_enter_object(&it[0], &it[1]) <= 0) return -EINVAL; while (spa_json_get_string(&it[1], k, sizeof(k)-1) > 0) { if (spa_streq(k, key)) { if (spa_json_get_string(&it[1], value, len) <= 0) continue; return 0; } else { if (spa_json_next(&it[1], &v) <= 0) break; } } return -ENOENT; } static int metadata_property(void *object, uint32_t subject, const char *key, const char *type, const char *value) { struct impl *impl = object; if (subject == PW_ID_CORE) { char name[1024]; if (key && value && json_object_find(value, "name", name, sizeof(name)) < 0) return 0; if (key == NULL || spa_streq(key, DEFAULT_AUDIO_SINK_KEY)) { free(impl->default_sink); impl->default_sink = (key && value) ? strdup(name) : NULL; /* Switch also when default output changes */ switch_profile_if_needed(impl); } } return 0; } static const struct pw_metadata_events metadata_events = { PW_VERSION_METADATA_EVENTS, .property = metadata_property, }; int sm_bluez5_autoswitch_start(struct sm_media_session *session) { struct impl *impl; int res; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; impl->context = session->context; impl->properties = pw_properties_new(NULL, NULL); if (impl->properties == NULL) { free(impl); return -ENOMEM; } if ((res = sm_media_session_load_state(impl->session, SESSION_KEY, impl->properties)) < 0) pw_log_info("can't load "SESSION_KEY" state: %s", spa_strerror(res)); sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); if (session->metadata) { pw_metadata_add_listener(session->metadata, &impl->meta_listener, &metadata_events, impl); } return 0; } 07070100000064000081A40000000000000000000000016178A88C00004AC6000000000000000000000000000000000000002900000000media-session-0.4.1/src/bluez-endpoint.c/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/utils/result.h> #include <spa/utils/names.h> #include <spa/utils/keys.h> #include <spa/utils/string.h> #include <spa/param/audio/format-utils.h> #include <spa/param/props.h> #include <spa/debug/dict.h> #include <spa/debug/pod.h> #include "pipewire/pipewire.h" #include <pipewire/extensions/session-manager.h> #include "media-session.h" /** \page page_media_session_module_bluez_endpoint Media Session Module: BlueZ Endpoint */ #define NAME "bluez-endpoint" #define SESSION_KEY "bluez-endpoint" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct endpoint { struct spa_list link; struct impl *impl; struct pw_properties *props; struct node *node; struct spa_hook listener; struct pw_client_endpoint *client_endpoint; struct spa_hook proxy_listener; struct spa_hook client_endpoint_listener; struct pw_endpoint_info info; struct spa_param_info params[5]; struct endpoint *monitor; struct spa_audio_info format; struct spa_list stream_list; }; struct stream { struct spa_list link; struct endpoint *endpoint; struct pw_properties *props; struct pw_endpoint_stream_info info; struct spa_audio_info format; unsigned int active:1; }; struct node { struct impl *impl; struct sm_node *node; struct device *device; struct endpoint *endpoint; }; struct device { struct impl *impl; uint32_t id; struct sm_device *device; struct spa_hook listener; struct spa_list endpoint_list; }; struct impl { struct sm_media_session *session; struct spa_hook listener; }; static int client_endpoint_set_session_id(void *object, uint32_t id) { struct endpoint *endpoint = object; endpoint->info.session_id = id; return 0; } static int client_endpoint_set_param(void *object, uint32_t id, uint32_t flags, const struct spa_pod *param) { struct endpoint *endpoint = object; struct impl *impl = endpoint->impl; pw_log_debug("%p: endpoint %p set param %d", impl, endpoint, id); return pw_node_set_param((struct pw_node*)endpoint->node->node->obj.proxy, id, flags, param); } static int client_endpoint_stream_set_param(void *object, uint32_t stream_id, uint32_t id, uint32_t flags, const struct spa_pod *param) { return -ENOTSUP; } static int stream_set_active(struct endpoint *endpoint, struct stream *stream, bool active) { char buf[1024]; struct spa_pod_builder b = { 0, }; struct spa_pod *param; if (stream->active == active) return 0; if (active) { stream->format.info.raw.rate = 48000; spa_pod_builder_init(&b, buf, sizeof(buf)); param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &stream->format.info.raw); param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(endpoint->info.direction), SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp), SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(true), SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, param); pw_node_set_param((struct pw_node*)endpoint->node->node->obj.proxy, SPA_PARAM_PortConfig, 0, param); } stream->active = active; return 0; } static int client_endpoint_create_link(void *object, const struct spa_dict *props) { struct endpoint *endpoint = object; struct impl *impl = endpoint->impl; struct pw_properties *p; int res; pw_log_debug("%p: endpoint %p", impl, endpoint); if (props == NULL) return -EINVAL; p = pw_properties_new_dict(props); if (p == NULL) return -errno; if (endpoint->info.direction == PW_DIRECTION_OUTPUT) { const char *str; struct sm_object *obj; str = spa_dict_lookup(props, PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT); if (str == NULL) { pw_log_warn("%p: no target endpoint given", impl); res = -EINVAL; goto exit; } obj = sm_media_session_find_object(impl->session, atoi(str)); if (obj == NULL || !spa_streq(obj->type, PW_TYPE_INTERFACE_Endpoint)) { pw_log_warn("%p: could not find endpoint %s (%p)", impl, str, obj); res = -EINVAL; goto exit; } pw_properties_setf(p, PW_KEY_LINK_OUTPUT_NODE, "%d", endpoint->node->node->info->id); pw_properties_setf(p, PW_KEY_LINK_OUTPUT_PORT, "-1"); pw_endpoint_create_link((struct pw_endpoint*)obj->proxy, &p->dict); } else { pw_properties_setf(p, PW_KEY_LINK_INPUT_NODE, "%d", endpoint->node->node->info->id); pw_properties_setf(p, PW_KEY_LINK_INPUT_PORT, "-1"); sm_media_session_create_links(impl->session, &p->dict); } res = 0; exit: pw_properties_free(p); return res; } static const struct pw_client_endpoint_events client_endpoint_events = { PW_VERSION_CLIENT_ENDPOINT_EVENTS, .set_session_id = client_endpoint_set_session_id, .set_param = client_endpoint_set_param, .stream_set_param = client_endpoint_stream_set_param, .create_link = client_endpoint_create_link, }; static struct stream *endpoint_add_stream(struct endpoint *endpoint) { struct stream *s; const char *str; s = calloc(1, sizeof(*s)); if (s == NULL) return NULL; s->props = pw_properties_new(NULL, NULL); s->endpoint = endpoint; if ((str = pw_properties_get(endpoint->props, PW_KEY_MEDIA_CLASS)) != NULL) pw_properties_set(s->props, PW_KEY_MEDIA_CLASS, str); if ((str = pw_properties_get(endpoint->props, PW_KEY_PRIORITY_SESSION)) != NULL) pw_properties_set(s->props, PW_KEY_PRIORITY_SESSION, str); if (endpoint->info.direction == PW_DIRECTION_OUTPUT) { if (endpoint->monitor != NULL) pw_properties_set(s->props, PW_KEY_ENDPOINT_STREAM_NAME, "Monitor"); else pw_properties_set(s->props, PW_KEY_ENDPOINT_STREAM_NAME, "Capture"); } else { pw_properties_set(s->props, PW_KEY_ENDPOINT_STREAM_NAME, "Playback"); } s->info.version = PW_VERSION_ENDPOINT_STREAM_INFO; s->info.id = endpoint->info.n_streams; s->info.endpoint_id = endpoint->info.id; s->info.name = (char*)pw_properties_get(s->props, PW_KEY_ENDPOINT_STREAM_NAME); s->info.change_mask = PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS; s->info.props = &s->props->dict; s->format = endpoint->format; pw_log_debug("stream %d", s->info.id); pw_client_endpoint_stream_update(endpoint->client_endpoint, s->info.id, PW_CLIENT_ENDPOINT_STREAM_UPDATE_INFO, 0, NULL, &s->info); spa_list_append(&endpoint->stream_list, &s->link); endpoint->info.n_streams++; return s; } static void destroy_stream(struct stream *stream) { struct endpoint *endpoint = stream->endpoint; pw_client_endpoint_stream_update(endpoint->client_endpoint, stream->info.id, PW_CLIENT_ENDPOINT_STREAM_UPDATE_DESTROYED, 0, NULL, &stream->info); spa_list_remove(&stream->link); endpoint->info.n_streams--; pw_properties_free(stream->props); free(stream); } static void update_params(void *data) { uint32_t n_params; const struct spa_pod **params; struct endpoint *endpoint = data; struct sm_node *node = endpoint->node->node; struct sm_param *p; pw_log_debug("%p: endpoint", endpoint); params = alloca(sizeof(struct spa_pod *) * node->n_params); n_params = 0; spa_list_for_each(p, &node->param_list, link) { switch (p->id) { case SPA_PARAM_Props: case SPA_PARAM_PropInfo: params[n_params++] = p->param; break; default: break; } } pw_client_endpoint_update(endpoint->client_endpoint, PW_CLIENT_ENDPOINT_UPDATE_PARAMS | PW_CLIENT_ENDPOINT_UPDATE_INFO, n_params, params, &endpoint->info); } static struct endpoint *create_endpoint(struct node *node, struct endpoint *monitor); static void object_update(void *data) { struct endpoint *endpoint = data; struct impl *impl = endpoint->impl; struct sm_node *node = endpoint->node->node; pw_log_debug("%p: endpoint %p", impl, endpoint); if (node->obj.changed & SM_NODE_CHANGE_MASK_PARAMS) update_params(endpoint); } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static void complete_endpoint(void *data) { struct endpoint *endpoint = data; struct stream *stream; struct sm_param *p; pw_log_debug("endpoint %p: complete", endpoint); spa_list_for_each(p, &endpoint->node->node->param_list, link) { struct spa_audio_info info = { 0, }; if (p->id != SPA_PARAM_EnumFormat) continue; if (spa_format_parse(p->param, &info.media_type, &info.media_subtype) < 0) continue; if (info.media_type != SPA_MEDIA_TYPE_audio || info.media_subtype != SPA_MEDIA_SUBTYPE_raw) continue; spa_pod_object_fixate((struct spa_pod_object*)p->param); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, p->param); if (spa_format_audio_raw_parse(p->param, &info.info.raw) < 0) continue; if (endpoint->format.info.raw.channels < info.info.raw.channels) endpoint->format = info; } pw_client_endpoint_update(endpoint->client_endpoint, PW_CLIENT_ENDPOINT_UPDATE_INFO, 0, NULL, &endpoint->info); stream = endpoint_add_stream(endpoint); if (endpoint->info.direction == PW_DIRECTION_INPUT) { struct endpoint *monitor; /* make monitor for sinks */ monitor = create_endpoint(endpoint->node, endpoint); if (monitor == NULL) return; endpoint_add_stream(monitor); } stream_set_active(endpoint, stream, true); sm_object_add_listener(&endpoint->node->node->obj, &endpoint->listener, &object_events, endpoint); } static void proxy_destroy(void *data) { struct endpoint *endpoint = data; struct stream *s; spa_list_consume(s, &endpoint->stream_list, link) destroy_stream(s); pw_properties_free(endpoint->props); spa_list_remove(&endpoint->link); spa_hook_remove(&endpoint->proxy_listener); spa_hook_remove(&endpoint->client_endpoint_listener); endpoint->client_endpoint = NULL; } static void proxy_bound(void *data, uint32_t id) { struct endpoint *endpoint = data; endpoint->info.id = id; } static const struct pw_proxy_events proxy_events = { PW_VERSION_PROXY_EVENTS, .destroy = proxy_destroy, .bound = proxy_bound, }; static struct endpoint *create_endpoint(struct node *node, struct endpoint *monitor) { struct impl *impl = node->impl; struct device *device = node->device; struct pw_properties *props; struct endpoint *endpoint; struct pw_proxy *proxy; const char *str, *media_class = NULL, *name = NULL; uint32_t subscribe[4], n_subscribe = 0; struct pw_properties *pr = node->node->obj.props; enum pw_direction direction; if (pr == NULL) { errno = EINVAL; return NULL; } if ((media_class = pw_properties_get(pr, PW_KEY_MEDIA_CLASS)) == NULL) { errno = EINVAL; return NULL; } if (strstr(media_class, "Source") != NULL) { direction = PW_DIRECTION_OUTPUT; } else if (strstr(media_class, "Sink") != NULL) { direction = PW_DIRECTION_INPUT; } else { errno = EINVAL; return NULL; } props = pw_properties_new(NULL, NULL); if (props == NULL) return NULL; if (monitor != NULL) { pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source"); direction = PW_DIRECTION_OUTPUT; } else { pw_properties_set(props, PW_KEY_MEDIA_CLASS, media_class); } if ((str = pw_properties_get(pr, PW_KEY_PRIORITY_SESSION)) != NULL) pw_properties_set(props, PW_KEY_PRIORITY_SESSION, str); if ((name = pw_properties_get(pr, PW_KEY_NODE_DESCRIPTION)) != NULL) { if (monitor != NULL) { pw_properties_setf(props, PW_KEY_ENDPOINT_NAME, "Monitor of %s", monitor->info.name); pw_properties_setf(props, PW_KEY_ENDPOINT_MONITOR, "%d", monitor->info.id); } else { pw_properties_set(props, PW_KEY_ENDPOINT_NAME, name); } } if ((str = pw_properties_get(pr, PW_KEY_DEVICE_ICON_NAME)) != NULL) pw_properties_set(props, PW_KEY_ENDPOINT_ICON_NAME, str); proxy = sm_media_session_create_object(impl->session, "client-endpoint", PW_TYPE_INTERFACE_ClientEndpoint, PW_VERSION_CLIENT_ENDPOINT, &props->dict, sizeof(*endpoint)); if (proxy == NULL) { pw_properties_free(props); return NULL; } endpoint = pw_proxy_get_user_data(proxy); endpoint->impl = impl; endpoint->node = node; endpoint->monitor = monitor; endpoint->props = props; endpoint->client_endpoint = (struct pw_client_endpoint *) proxy; endpoint->info.version = PW_VERSION_ENDPOINT_INFO; endpoint->info.name = (char*)pw_properties_get(endpoint->props, PW_KEY_ENDPOINT_NAME); endpoint->info.media_class = (char*)pw_properties_get(endpoint->props, PW_KEY_MEDIA_CLASS); endpoint->info.session_id = impl->session->session->obj.id; endpoint->info.direction = direction; endpoint->info.flags = 0; endpoint->info.change_mask = PW_ENDPOINT_CHANGE_MASK_STREAMS | PW_ENDPOINT_CHANGE_MASK_SESSION | PW_ENDPOINT_CHANGE_MASK_PROPS | PW_ENDPOINT_CHANGE_MASK_PARAMS; endpoint->info.n_streams = 0; endpoint->info.props = &endpoint->props->dict; endpoint->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); endpoint->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); endpoint->info.params = endpoint->params; endpoint->info.n_params = 2; spa_list_init(&endpoint->stream_list); pw_log_debug("%p: new endpoint %p for bluez node %p", impl, endpoint, node); pw_proxy_add_listener(proxy, &endpoint->proxy_listener, &proxy_events, endpoint); pw_client_endpoint_add_listener(endpoint->client_endpoint, &endpoint->client_endpoint_listener, &client_endpoint_events, endpoint); subscribe[n_subscribe++] = SPA_PARAM_EnumFormat; subscribe[n_subscribe++] = SPA_PARAM_Props; subscribe[n_subscribe++] = SPA_PARAM_PropInfo; pw_log_debug("%p: endpoint %p proxy %p subscribe %d params", impl, endpoint, node->node->obj.proxy, n_subscribe); pw_node_subscribe_params((struct pw_node*)node->node->obj.proxy, subscribe, n_subscribe); spa_list_append(&device->endpoint_list, &endpoint->link); if (monitor == NULL) sm_media_session_sync(impl->session, complete_endpoint, endpoint); return endpoint; } static void destroy_endpoint(struct endpoint *endpoint) { if (endpoint->client_endpoint) pw_proxy_destroy((struct pw_proxy*)endpoint->client_endpoint); } /** fallback, one stream for each node */ static int setup_bluez_endpoint(struct device *device) { struct impl *impl = device->impl; struct sm_node *n; struct sm_device *d = device->device; pw_log_debug("%p: device %p fallback", impl, d); spa_list_for_each(n, &d->node_list, link) { struct node *node; pw_log_debug("%p: device %p has node %p", impl, d, n); node = sm_object_add_data(&n->obj, SESSION_KEY, sizeof(struct node)); node->device = device; node->node = n; node->impl = impl; node->endpoint = create_endpoint(node, NULL); if (node->endpoint == NULL) return -errno; } return 0; } static int activate_device(struct device *device) { return setup_bluez_endpoint(device); } static int deactivate_device(struct device *device) { struct endpoint *e; spa_list_consume(e, &device->endpoint_list, link) destroy_endpoint(e); return 0; } static void device_update(void *data) { struct device *device = data; struct impl *impl = device->impl; pw_log_debug("%p: device %p %08x %08x", impl, device, device->device->obj.avail, device->device->obj.changed); if (!SPA_FLAG_IS_SET(device->device->obj.avail, SM_DEVICE_CHANGE_MASK_INFO | SM_DEVICE_CHANGE_MASK_NODES | SM_DEVICE_CHANGE_MASK_PARAMS)) return; // if (SPA_FLAG_IS_SET(device->device->obj.changed, // SM_DEVICE_CHANGE_MASK_NODES | // SM_DEVICE_CHANGE_MASK_PARAMS)) { activate_device(device); // } } static const struct sm_object_events device_events = { SM_VERSION_OBJECT_EVENTS, .update = device_update }; static int handle_device(struct impl *impl, struct sm_object *obj) { const char *media_class, *str; struct device *device; if (obj->props == NULL) return 0; media_class = pw_properties_get(obj->props, PW_KEY_MEDIA_CLASS); str = pw_properties_get(obj->props, PW_KEY_DEVICE_API); pw_log_debug("%p: device "PW_KEY_MEDIA_CLASS":%s api:%s", impl, media_class, str); if (!spa_strstartswith(media_class, "Audio/")) return 0; if (!spa_streq(str, "bluez5")) return 0; device = sm_object_add_data(obj, SESSION_KEY, sizeof(struct device)); device->impl = impl; device->id = obj->id; device->device = (struct sm_device*)obj; spa_list_init(&device->endpoint_list); pw_log_debug("%p: found bluez device %d media_class %s", impl, obj->id, media_class); sm_object_add_listener(obj, &device->listener, &device_events, device); return 0; } static void destroy_device(struct impl *impl, struct device *device) { deactivate_device(device); spa_hook_remove(&device->listener); sm_object_remove_data((struct sm_object*)device->device, SESSION_KEY); } static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; int res; if (spa_streq(object->type, PW_TYPE_INTERFACE_Device)) res = handle_device(impl, object); else res = 0; if (res < 0) { pw_log_warn("%p: can't handle global %d: %s", impl, object->id, spa_strerror(res)); } } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; if (spa_streq(object->type, PW_TYPE_INTERFACE_Device)) { struct device *device; if ((device = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_device(impl, device); } } static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->listener); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .destroy = session_destroy, }; int sm_bluez5_endpoint_start(struct sm_media_session *session) { struct impl *impl; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; sm_media_session_add_listener(session, &impl->listener, &session_events, impl); return 0; } 07070100000065000081A40000000000000000000000016178A88C00004B48000000000000000000000000000000000000002800000000media-session-0.4.1/src/bluez-monitor.c/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <spa/monitor/device.h> #include <spa/monitor/event.h> #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/utils/result.h> #include <spa/utils/names.h> #include <spa/utils/keys.h> #include <spa/utils/string.h> #include <spa/pod/builder.h> #include <spa/pod/parser.h> #include <spa/param/props.h> #include <spa/debug/dict.h> #include <spa/debug/pod.h> #include "pipewire/impl.h" #include "media-session.h" /** \page page_media_session_module_bluez_monitor Media Session Module: BlueZ Monitor */ #define NAME "bluez5-monitor" #define SESSION_CONF "bluez-monitor.conf" #define FEATURES_CONF "bluez-hardware.conf" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct device; struct node { struct impl *impl; enum pw_direction direction; struct device *device; struct spa_list link; uint32_t id; struct pw_properties *props; struct pw_impl_node *adapter; struct sm_node *snode; }; struct device { struct impl *impl; struct spa_list link; uint32_t id; uint32_t device_id; int priority; int profile; struct pw_properties *props; struct spa_handle *handle; struct spa_device *device; struct spa_hook device_listener; struct sm_device *sdevice; struct spa_hook listener; unsigned int appeared:1; struct spa_list node_list; }; struct impl { struct sm_media_session *session; struct spa_hook session_listener; bool have_info; bool seat_active; struct pw_properties *conf; struct pw_properties *props; struct spa_handle *handle; struct spa_device *monitor; struct spa_hook listener; struct spa_list device_list; }; static struct node *bluez5_find_node(struct device *device, uint32_t id) { struct node *node; spa_list_for_each(node, &device->node_list, link) { if (node->id == id) return node; } return NULL; } static void update_icon_name(struct pw_properties *p, bool is_sink) { const char *s, *d = NULL, *bus; if ((s = pw_properties_get(p, PW_KEY_DEVICE_FORM_FACTOR))) { if (spa_streq(s, "microphone")) d = "audio-input-microphone"; else if (spa_streq(s, "webcam")) d = "camera-web"; else if (spa_streq(s, "computer")) d = "computer"; else if (spa_streq(s, "handset")) d = "phone"; else if (spa_streq(s, "portable")) d = "multimedia-player"; else if (spa_streq(s, "tv")) d = "video-display"; else if (spa_streq(s, "headset")) d = "audio-headset"; else if (spa_streq(s, "headphone")) d = "audio-headphones"; else if (spa_streq(s, "speaker")) d = "audio-speakers"; else if (spa_streq(s, "hands-free")) d = "audio-handsfree"; } if (!d) if ((s = pw_properties_get(p, PW_KEY_DEVICE_CLASS))) if (spa_streq(s, "modem")) d = "modem"; if (!d) { if (is_sink) d = "audio-card"; else d = "audio-input-microphone"; } if ((s = pw_properties_get(p, "device.profile.name")) != NULL) { if (strstr(s, "analog")) s = "-analog"; else if (strstr(s, "iec958")) s = "-iec958"; else if (strstr(s, "hdmi")) s = "-hdmi"; else s = NULL; } bus = pw_properties_get(p, PW_KEY_DEVICE_BUS); pw_properties_setf(p, PW_KEY_DEVICE_ICON_NAME, "%s%s%s%s", d, s ? s : "", bus ? "-" : "", bus ? bus : ""); } static void bluez5_update_node(struct device *device, struct node *node, const struct spa_device_object_info *info) { pw_log_debug("update node %u", node->id); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); } static struct node *bluez5_create_node(struct device *device, uint32_t id, const struct spa_device_object_info *info) { struct node *node; struct impl *impl = device->impl; struct pw_context *context = impl->session->context; struct pw_impl_factory *factory; int res; const char *prefix, *str, *profile, *rules; int priority; char tmp[1024]; bool is_sink; pw_log_debug("new node %u", id); if (!spa_streq(info->type, SPA_TYPE_INTERFACE_Node)) { errno = EINVAL; return NULL; } node = calloc(1, sizeof(*node)); if (node == NULL) { res = -errno; goto exit; } node->props = pw_properties_new_dict(info->props); if (pw_properties_get(node->props, PW_KEY_DEVICE_FORM_FACTOR) == NULL) pw_properties_set(node->props, PW_KEY_DEVICE_FORM_FACTOR, pw_properties_get(device->props, PW_KEY_DEVICE_FORM_FACTOR)); if (pw_properties_get(node->props, PW_KEY_DEVICE_BUS) == NULL) pw_properties_set(node->props, PW_KEY_DEVICE_BUS, pw_properties_get(device->props, PW_KEY_DEVICE_BUS)); str = pw_properties_get(device->props, SPA_KEY_DEVICE_DESCRIPTION); if (str == NULL) str = pw_properties_get(device->props, SPA_KEY_DEVICE_NAME); if (str == NULL) str = pw_properties_get(device->props, SPA_KEY_DEVICE_NICK); if (str == NULL) str = pw_properties_get(device->props, SPA_KEY_DEVICE_ALIAS); if (str == NULL) str = "bluetooth-device"; pw_properties_setf(node->props, PW_KEY_DEVICE_ID, "%d", device->device_id); pw_properties_set(node->props, PW_KEY_NODE_DESCRIPTION, sm_media_session_sanitize_description(tmp, sizeof(tmp), ' ', "%s", str)); profile = pw_properties_get(node->props, SPA_KEY_API_BLUEZ5_PROFILE); if (profile == NULL) profile = "unknown"; str = pw_properties_get(node->props, SPA_KEY_API_BLUEZ5_ADDRESS); if (str == NULL) str = pw_properties_get(device->props, SPA_KEY_DEVICE_NAME); is_sink = strstr(info->factory_name, "sink") != NULL; if (is_sink) prefix = "bluez_output"; else if (strstr(info->factory_name, "source") != NULL) prefix = "bluez_input"; else prefix = info->factory_name; pw_properties_set(node->props, PW_KEY_NODE_NAME, sm_media_session_sanitize_name(tmp, sizeof(tmp), '_', "%s.%s.%s", prefix, str, profile)); pw_properties_set(node->props, PW_KEY_FACTORY_NAME, info->factory_name); if (pw_properties_get(node->props, PW_KEY_PRIORITY_DRIVER) == NULL) { priority = device->priority + 10; if (strstr(info->factory_name, "source") != NULL) priority += 1000; pw_properties_setf(node->props, PW_KEY_PRIORITY_DRIVER, "%d", priority); pw_properties_setf(node->props, PW_KEY_PRIORITY_SESSION, "%d", priority); } if (pw_properties_get(node->props, PW_KEY_DEVICE_ICON_NAME) == NULL) update_icon_name(node->props, is_sink); node->impl = impl; node->device = device; node->id = id; if ((rules = pw_properties_get(impl->conf, "rules")) != NULL) sm_media_session_match_rules(rules, strlen(rules), node->props); factory = pw_context_find_factory(context, "adapter"); if (factory == NULL) { pw_log_error("no adapter factory found"); res = -EIO; goto clean_node; } node->adapter = pw_impl_factory_create_object(factory, NULL, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, pw_properties_copy(node->props), 0); if (node->adapter == NULL) { res = -errno; goto clean_node; } node->snode = sm_media_session_export_node(impl->session, &node->props->dict, node->adapter); if (node->snode == NULL) { res = -errno; goto clean_node; } spa_list_append(&device->node_list, &node->link); bluez5_update_node(device, node, info); return node; clean_node: pw_properties_free(node->props); free(node); exit: errno = -res; return NULL; } static void bluez5_remove_node(struct device *device, struct node *node) { pw_log_debug("remove node %u", node->id); spa_list_remove(&node->link); sm_object_destroy(&node->snode->obj); pw_impl_node_destroy(node->adapter); pw_properties_free(node->props); free(node); } static void bluez5_device_object_info(void *data, uint32_t id, const struct spa_device_object_info *info) { struct device *device = data; struct node *node; node = bluez5_find_node(device, id); if (info == NULL) { if (node == NULL) { pw_log_warn("device %p: unknown node %u", device, id); return; } bluez5_remove_node(device, node); } else if (node == NULL) { bluez5_create_node(device, id, info); } else { bluez5_update_node(device, node, info); } } static void bluez_device_event(void *data, const struct spa_event *event) { struct device *device = data; struct node *node; uint32_t id, type; struct spa_pod *props = NULL; if (spa_pod_parse_object(&event->pod, SPA_TYPE_EVENT_Device, &type, SPA_EVENT_DEVICE_Object, SPA_POD_Int(&id), SPA_EVENT_DEVICE_Props, SPA_POD_OPT_Pod(&props)) < 0) return; if ((node = bluez5_find_node(device, id)) == NULL) { pw_log_warn("device %p: unknown node %d", device, id); return; } switch (type) { case SPA_DEVICE_EVENT_ObjectConfig: if (props != NULL) { struct spa_node *adapter; adapter = pw_impl_node_get_implementation(node->adapter); spa_node_set_param(adapter, SPA_PARAM_Props, 0, props); } break; default: break; } } static const struct spa_device_events bluez5_device_events = { SPA_VERSION_DEVICE_EVENTS, .object_info = bluez5_device_object_info, .event = bluez_device_event, }; static struct device *bluez5_find_device(struct impl *impl, uint32_t id) { struct device *device; spa_list_for_each(device, &impl->device_list, link) { if (device->id == id) return device; } return NULL; } static int update_device_props(struct device *device) { struct pw_properties *p = device->props; const char *s; char temp[32], tmp[1024]; s = pw_properties_get(p, SPA_KEY_DEVICE_NAME); if (s == NULL) s = pw_properties_get(p, SPA_KEY_API_BLUEZ5_ADDRESS); if (s == NULL) s = pw_properties_get(p, SPA_KEY_DEVICE_DESCRIPTION); if (s == NULL) { snprintf(temp, sizeof(temp), "%d", device->id); s = temp; } if (spa_strstartswith(s, "bluez_card.")) s += strlen("bluez_card."); pw_properties_set(p, PW_KEY_DEVICE_NAME, sm_media_session_sanitize_name(tmp, sizeof(tmp), '_', "bluez_card.%s", s)); if (pw_properties_get(p, SPA_KEY_DEVICE_ICON_NAME) == NULL) update_icon_name(p, true); return 0; } static void device_destroy(void *data) { struct device *device = data; struct node *node; pw_log_debug("device %p destroy", device); spa_hook_remove(&device->listener); if (device->appeared) { device->appeared = false; spa_hook_remove(&device->device_listener); } spa_list_consume(node, &device->node_list, link) bluez5_remove_node(device, node); } static void device_update(void *data) { struct device *device = data; pw_log_debug("device %p appeared %d %d", device, device->appeared, device->profile); if (device->appeared) return; device->device_id = device->sdevice->obj.id; device->appeared = true; spa_device_add_listener(device->device, &device->device_listener, &bluez5_device_events, device); sm_object_sync_update(&device->sdevice->obj); } static const struct sm_object_events device_events = { SM_VERSION_OBJECT_EVENTS, .destroy = device_destroy, .update = device_update, }; static struct device *bluez5_create_device(struct impl *impl, uint32_t id, const struct spa_device_object_info *info) { struct pw_context *context = impl->session->context; struct device *device; struct spa_handle *handle; int res; void *iface; const char *rules, *str; pw_log_debug("new device %u", id); if (!spa_streq(info->type, SPA_TYPE_INTERFACE_Device)) { errno = EINVAL; return NULL; } device = calloc(1, sizeof(*device)); if (device == NULL) { res = -errno; goto exit; } device->impl = impl; device->id = id; device->priority = 1000; device->props = pw_properties_new_dict(info->props); update_device_props(device); spa_list_init(&device->node_list); if ((rules = pw_properties_get(impl->conf, "rules")) != NULL) sm_media_session_match_rules(rules, strlen(rules), device->props); /* Propagate the msbc-support global property if it exists and is not * overloaded by a device specific one */ if ((str = pw_properties_get(impl->props, "bluez5.msbc-support")) != NULL && pw_properties_get(device->props, "bluez5.msbc-support") == NULL) pw_properties_set(device->props, "bluez5.msbc-support", str); handle = pw_context_load_spa_handle(context, info->factory_name, &device->props->dict); if (handle == NULL) { res = -errno; pw_log_error("can't make factory instance: %m"); goto clean_device; } if ((res = spa_handle_get_interface(handle, info->type, &iface)) < 0) { pw_log_error("can't get %s interface: %s", info->type, spa_strerror(res)); goto unload_handle; } device->handle = handle; device->device = iface; spa_list_append(&impl->device_list, &device->link); return device; unload_handle: pw_unload_spa_handle(handle); clean_device: pw_properties_free(device->props); free(device); exit: errno = -res; return NULL; } static void bluez5_device_free(struct device *device) { if (device->sdevice) { sm_object_destroy(&device->sdevice->obj); device->sdevice = NULL; } spa_list_remove(&device->link); pw_unload_spa_handle(device->handle); pw_properties_free(device->props); free(device); } static void bluez5_remove_device(struct impl *impl, struct device *device) { pw_log_debug("remove device %u", device->id); bluez5_device_free(device); } static void bluez5_update_device(struct impl *impl, struct device *device, const struct spa_device_object_info *info) { bool connected; const char *str; if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); pw_log_debug("update device %u", device->id); pw_properties_update(device->props, info->props); update_device_props(device); str = spa_dict_lookup(info->props, SPA_KEY_API_BLUEZ5_CONNECTION); connected = str != NULL && spa_streq(str, "connected"); /* Export device after bluez profiles get connected */ if (device->sdevice == NULL && connected) { device->sdevice = sm_media_session_export_device(impl->session, &device->props->dict, device->device); if (device->sdevice == NULL) { bluez5_device_free(device); return; } sm_object_add_listener(&device->sdevice->obj, &device->listener, &device_events, device); } else if (device->sdevice != NULL && !connected) { sm_object_destroy(&device->sdevice->obj); device->sdevice = NULL; } } static void bluez5_enum_object_info(void *data, uint32_t id, const struct spa_device_object_info *info) { struct impl *impl = data; struct device *device; device = bluez5_find_device(impl, id); if (info == NULL) { if (device == NULL) return; bluez5_remove_device(impl, device); } else if (device == NULL) { if (bluez5_create_device(impl, id, info) == NULL) return; } else { bluez5_update_device(impl, device, info); } } static const struct spa_device_events bluez5_enum_callbacks = { SPA_VERSION_DEVICE_EVENTS, .object_info = bluez5_enum_object_info, }; static void unload_bluez_handle(struct impl *impl) { struct device *device; if (impl->handle == NULL) return; spa_list_consume(device, &impl->device_list, link) bluez5_device_free(device); spa_hook_remove(&impl->listener); pw_unload_spa_handle(impl->handle); impl->handle = NULL; } static int load_bluez_handle(struct impl *impl) { struct pw_context *context = impl->session->context; void *iface; int res; if (impl->handle != NULL || !impl->seat_active || !impl->have_info) return 0; impl->handle = pw_context_load_spa_handle(context, SPA_NAME_API_BLUEZ5_ENUM_DBUS, &impl->props->dict); if (impl->handle == NULL) { res = -errno; pw_log_info("can't load %s: %m", SPA_NAME_API_BLUEZ5_ENUM_DBUS); goto fail; } if ((res = spa_handle_get_interface(impl->handle, SPA_TYPE_INTERFACE_Device, &iface)) < 0) { pw_log_error("can't get Device interface: %s", spa_strerror(res)); goto fail; } impl->monitor = iface; spa_device_add_listener(impl->monitor, &impl->listener, &bluez5_enum_callbacks, impl); return 0; fail: if (impl->handle) pw_unload_spa_handle(impl->handle); impl->handle = NULL; return res; } static void session_info(void *data, const struct pw_core_info *info) { struct impl *impl = data; if (info && (info->change_mask & PW_CORE_CHANGE_MASK_PROPS)) { const char *str; if ((str = spa_dict_lookup(info->props, "default.clock.rate")) != NULL && pw_properties_get(impl->props, "bluez5.default.rate") == NULL) { pw_properties_set(impl->props, "bluez5.default.rate", str); } impl->have_info = true; load_bluez_handle(impl); } } static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->session_listener); unload_bluez_handle(impl); pw_properties_free(impl->props); pw_properties_free(impl->conf); free(impl); } static void seat_active(void *data, bool active) { struct impl *impl = data; impl->seat_active = active; if (impl->seat_active) { pw_log_info("seat active, starting bluetooth"); load_bluez_handle(impl); } else { pw_log_info("seat not active, stopping bluetooth"); unload_bluez_handle(impl); } } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .info = session_info, .destroy = session_destroy, .seat_active = seat_active, }; int sm_bluez5_monitor_start(struct sm_media_session *session) { int res; struct impl *impl; const char *str; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) { res = -errno; goto out; } impl->session = session; impl->seat_active = true; spa_list_init(&impl->device_list); if ((impl->conf = pw_properties_new(NULL, NULL)) == NULL) { res = -errno; goto out_free; } if ((res = sm_media_session_load_conf(impl->session, SESSION_CONF, impl->conf)) < 0) pw_log_info("can't load "SESSION_CONF" config: %s", spa_strerror(res)); if ((impl->props = pw_properties_new(NULL, NULL)) == NULL) { res = -errno; goto out_free; } if ((str = pw_properties_get(impl->conf, "properties")) != NULL) pw_properties_update_string(impl->props, str, strlen(str)); pw_properties_set(impl->props, "api.bluez5.connection-info", "true"); sm_media_session_add_listener(session, &impl->session_listener, &session_events, impl); return 0; out_free: pw_properties_free(impl->conf); pw_properties_free(impl->props); free(impl); out: return res; } 07070100000066000081A40000000000000000000000016178A88C000015C4000000000000000000000000000000000000002800000000media-session-0.4.1/src/default-nodes.c/* PipeWire * * Copyright © 2020 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include <fcntl.h> #include <unistd.h> #include "config.h" #include <spa/utils/hook.h> #include <spa/utils/result.h> #include <spa/utils/string.h> #include "pipewire/pipewire.h" #include "pipewire/extensions/metadata.h" #include "media-session.h" /** \page page_media_session_module_default_nodes Media Session Module: Default Nodes */ #define NAME "default-nodes" #define SESSION_KEY "default-nodes" #define SAVE_INTERVAL 1 #define DEFAULT_CONFIG_AUDIO_SINK_KEY "default.configured.audio.sink" #define DEFAULT_CONFIG_AUDIO_SOURCE_KEY "default.configured.audio.source" #define DEFAULT_CONFIG_VIDEO_SOURCE_KEY "default.configured.video.source" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct impl { struct sm_media_session *session; struct spa_hook listener; struct pw_context *context; struct spa_source *idle_timeout; struct spa_hook meta_listener; struct pw_properties *properties; }; static bool is_default_key(const char *key) { return spa_streq(key, DEFAULT_CONFIG_AUDIO_SINK_KEY) || spa_streq(key, DEFAULT_CONFIG_AUDIO_SOURCE_KEY) || spa_streq(key, DEFAULT_CONFIG_VIDEO_SOURCE_KEY); } static void remove_idle_timeout(struct impl *impl) { struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); int res; if (impl->idle_timeout) { if ((res = sm_media_session_save_state(impl->session, SESSION_KEY, impl->properties)) < 0) pw_log_error("can't save "SESSION_KEY" state: %s", spa_strerror(res)); pw_loop_destroy_source(main_loop, impl->idle_timeout); impl->idle_timeout = NULL; } } static void idle_timeout(void *data, uint64_t expirations) { struct impl *impl = data; pw_log_debug("%p: idle timeout", impl); remove_idle_timeout(impl); } static void add_idle_timeout(struct impl *impl) { struct timespec value; struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); if (impl->idle_timeout == NULL) impl->idle_timeout = pw_loop_add_timer(main_loop, idle_timeout, impl); value.tv_sec = SAVE_INTERVAL; value.tv_nsec = 0; pw_loop_update_timer(main_loop, impl->idle_timeout, &value, NULL, false); } static int metadata_property(void *object, uint32_t subject, const char *key, const char *type, const char *value) { struct impl *impl = object; int changed = 0; if (subject == PW_ID_CORE) { if (key == NULL) { pw_properties_clear(impl->properties); changed++; } else { if (!is_default_key(key)) return 0; changed += pw_properties_set(impl->properties, key, value); } } if (changed) add_idle_timeout(impl); return 0; } static const struct pw_metadata_events metadata_events = { PW_VERSION_METADATA_EVENTS, .property = metadata_property, }; static void load_metadata(struct impl *impl) { const struct spa_dict_item *item; spa_dict_for_each(item, &impl->properties->dict) { if (!is_default_key(item->key)) continue; if (impl->session->metadata != NULL) { pw_log_info("restoring %s=%s", item->key, item->value); pw_metadata_set_property(impl->session->metadata, PW_ID_CORE, item->key, "Spa:String:JSON", item->value); } } } static void session_destroy(void *data) { struct impl *impl = data; remove_idle_timeout(impl); spa_hook_remove(&impl->listener); if (impl->session->metadata) spa_hook_remove(&impl->meta_listener); pw_properties_free(impl->properties); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .destroy = session_destroy, }; int sm_default_nodes_start(struct sm_media_session *session) { struct impl *impl; int res; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; impl->context = session->context; impl->properties = pw_properties_new(NULL, NULL); if (impl->properties == NULL) { free(impl); return -ENOMEM; } if ((res = sm_media_session_load_state(impl->session, SESSION_KEY, impl->properties)) < 0) pw_log_info("can't load "SESSION_KEY" state: %s", spa_strerror(res)); sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); if (session->metadata) { pw_metadata_add_listener(session->metadata, &impl->meta_listener, &metadata_events, impl); } load_metadata(impl); return 0; } 07070100000067000081A40000000000000000000000016178A88C00003730000000000000000000000000000000000000002A00000000media-session-0.4.1/src/default-profile.c/* PipeWire * * Copyright © 2020 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include <fcntl.h> #include <unistd.h> #include "config.h" #include <spa/utils/hook.h> #include <spa/utils/result.h> #include <spa/utils/json.h> #include <spa/utils/string.h> #include <spa/pod/parser.h> #include <spa/pod/builder.h> #include <spa/debug/pod.h> #include "pipewire/pipewire.h" #include "pipewire/extensions/metadata.h" #include "media-session.h" /** \page page_media_session_module_default_profile Media Session Module: Default Profile * * The default profile module restores a previously saved profile * or otherwise the best available profile. * * The module tracks the \ref SPA_PARAM_Profile parameter on devices * (excluding Bluetooth devices). * When the profile is changed by an external party (e.g. `pavucontrol`), that * profile is written to the state file. In the future, when the active * profile is `"off"`, the previously saved profile (if available) is restored. * * If no saved profile exists, the best profile is restored. The rules for * determining the best profile are: * - the highest-priority available profile, or, if no profiles are available, * - the highest-priority profile with availability unknown, or, if no such * profile exists, * - the `"off"` profile. * * \note The special profile named `"pro-audio"` is excluded from the above search. * * ## Module-specific properties * * This module stores its state in * `$XDG_CONFIG_HOME/pipewire/media-sesssion.d/default-profile`: * * - `default.profile.$devicename = { "name": "$profilename" }`: stores the default * profile for `$devicename` * * ## See also * See \ref spa_param_availability for availability values. */ #define NAME "default-profile" #define SESSION_KEY "default-profile" #define PREFIX "default.profile." #define SAVE_INTERVAL 1 PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct impl { struct timespec now; struct sm_media_session *session; struct spa_hook listener; struct pw_context *context; struct spa_source *idle_timeout; struct spa_hook meta_listener; struct pw_properties *properties; unsigned int restore_bluetooth:1; }; struct device { struct sm_device *obj; uint32_t id; struct impl *impl; char *name; char *key; struct spa_hook listener; unsigned int restore_saved_profile:1; uint32_t best_profile; uint32_t active_profile; }; static void remove_idle_timeout(struct impl *impl) { struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); int res; if (impl->idle_timeout) { if ((res = sm_media_session_save_state(impl->session, SESSION_KEY, impl->properties)) < 0) pw_log_error("can't save "SESSION_KEY" state: %s", spa_strerror(res)); pw_loop_destroy_source(main_loop, impl->idle_timeout); impl->idle_timeout = NULL; } } static void idle_timeout(void *data, uint64_t expirations) { struct impl *impl = data; pw_log_debug("%p: idle timeout", impl); remove_idle_timeout(impl); } static void add_idle_timeout(struct impl *impl) { struct timespec value; struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); if (impl->idle_timeout == NULL) impl->idle_timeout = pw_loop_add_timer(main_loop, idle_timeout, impl); value.tv_sec = SAVE_INTERVAL; value.tv_nsec = 0; pw_loop_update_timer(main_loop, impl->idle_timeout, &value, NULL, false); } struct profile { struct sm_param *p; uint32_t index; const char *name; uint32_t prio; uint32_t available; bool save; }; static int parse_profile(struct sm_param *p, struct profile *pr) { pr->p = p; pr->prio = 0; pr->available = SPA_PARAM_AVAILABILITY_unknown; pr->save = false; return spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_ParamProfile, NULL, SPA_PARAM_PROFILE_index, SPA_POD_Int(&pr->index), SPA_PARAM_PROFILE_name, SPA_POD_String(&pr->name), SPA_PARAM_PROFILE_priority, SPA_POD_OPT_Int(&pr->prio), SPA_PARAM_PROFILE_available, SPA_POD_OPT_Id(&pr->available), SPA_PARAM_PROFILE_save, SPA_POD_OPT_Bool(&pr->save)); } static int find_current_profile(struct device *dev, struct profile *pr) { struct sm_param *p; spa_list_for_each(p, &dev->obj->param_list, link) { if (p->id == SPA_PARAM_Profile && parse_profile(p, pr) >= 0) return 0; } return -ENOENT; } static int find_best_profile(struct device *dev, struct profile *pr) { struct sm_param *p; struct profile best, best_avail, best_unk, off; spa_zero(best); spa_zero(best_avail); spa_zero(best_unk); spa_zero(off); spa_list_for_each(p, &dev->obj->param_list, link) { struct profile t; if (p->id != SPA_PARAM_EnumProfile || parse_profile(p, &t) < 0) continue; if (t.name && spa_streq(t.name, "pro-audio")) continue; if (t.name && spa_streq(t.name, "off")) { off = t; } else if (t.available == SPA_PARAM_AVAILABILITY_yes) { if (best_avail.name == NULL || t.prio > best_avail.prio) best_avail = t; } else if (t.available != SPA_PARAM_AVAILABILITY_no) { if (best_unk.name == NULL || t.prio > best_unk.prio) best_unk = t; } } best = best_avail; if (best.name == NULL) best = best_unk; if (best.name == NULL) best = off; if (best.name == NULL) return -ENOENT; *pr = best; return 0; } static int find_saved_profile(struct device *dev, struct profile *pr) { struct spa_json it[2]; struct impl *impl = dev->impl; const char *json, *value; char name[1024] = "\0", key[128]; struct sm_param *p; json = pw_properties_get(impl->properties, dev->key); if (json == NULL) return -ENODEV; spa_json_init(&it[0], json, strlen(json)); if (spa_json_enter_object(&it[0], &it[1]) <= 0) return -EINVAL; while (spa_json_get_string(&it[1], key, sizeof(key)-1) > 0) { if (spa_streq(key, "name")) { if (spa_json_get_string(&it[1], name, sizeof(name)) <= 0) continue; } else { if (spa_json_next(&it[1], &value) <= 0) break; } } pw_log_debug("device '%s': find profile '%s'", dev->name, name); spa_list_for_each(p, &dev->obj->param_list, link) { if (p->id != SPA_PARAM_EnumProfile || parse_profile(p, pr) < 0) continue; if (spa_streq(pr->name, name)) return 0; } return -ENOENT; } static int set_profile(struct device *dev, struct profile *pr) { char buf[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); if (dev->active_profile == pr->index) return 0; pw_device_set_param((struct pw_device*)dev->obj->obj.proxy, SPA_PARAM_Profile, 0, spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile, SPA_PARAM_PROFILE_index, SPA_POD_Int(pr->index), SPA_PARAM_PROFILE_save, SPA_POD_Bool(pr->save))); sm_media_session_schedule_rescan(dev->impl->session); return 0; } static int handle_active_profile(struct device *dev) { struct impl *impl = dev->impl; struct profile pr; int res; /* check if current profile changed */ if ((res = find_current_profile(dev, &pr)) < 0) return res; /* when the active profile is off, always try to restore the saved * profile again */ if (spa_streq(pr.name, "off")) dev->restore_saved_profile = true; if (dev->active_profile == pr.index) { /* no change, we're done */ pw_log_info("device '%s': active profile '%s'", dev->name, pr.name); return 0; } /* we get here when we had configured a profile but something * else changed it, in that case, save it when asked. */ pw_log_info("device '%s': active profile changed to '%s'", dev->name, pr.name); dev->active_profile = pr.index; if (!pr.save) return 0; if (pw_properties_setf(impl->properties, dev->key, "{ \"name\": \"%s\" }", pr.name)) { pw_log_info("device '%s': active profile saved as '%s'", dev->name, pr.name); add_idle_timeout(impl); } return 0; } static int handle_profile_switch(struct device *dev) { struct profile saved, best; int res; bool changed = false; /* try to find the next best profile */ res = find_best_profile(dev, &best); if (res < 0) { pw_log_info("device '%s': can't find best profile: %s", dev->name, spa_strerror(res)); best.index = SPA_ID_INVALID; } else { changed = dev->best_profile != best.index; dev->best_profile = best.index; pw_log_info("device '%s': found best profile '%s' changed:%d", dev->name, best.name, changed); } if (dev->restore_saved_profile) { /* try to restore our saved profile */ res = find_saved_profile(dev, &saved); if (res >= 0) { /* we found a saved profile */ if (saved.available == SPA_PARAM_AVAILABILITY_no) { pw_log_info("device '%s': saved profile '%s' unavailable", dev->name, saved.name); } else { pw_log_info("device '%s': found saved profile '%s'", dev->name, saved.name); /* make sure we save again */ saved.save = true; best = saved; changed = true; } } else { pw_log_info("device '%s': no saved profile: %s", dev->name, spa_strerror(res)); } dev->restore_saved_profile = false; } if (best.index != SPA_ID_INVALID && changed) { if (dev->active_profile == best.index) { pw_log_info("device '%s': best profile '%s' is already active", dev->name, best.name); } else { pw_log_info("device '%s': restore best profile '%s' index %d", dev->name, best.name, best.index); set_profile(dev, &best); } } else if (res < 0) { pw_log_warn("device '%s': can't restore profile: %s", dev->name, spa_strerror(res)); } else { pw_log_info("device '%s': no profile switch needed", dev->name); } return 0; } static int handle_profile(struct device *dev) { /* check if current profile changed */ handle_active_profile(dev); /* check if we need to switch profile */ handle_profile_switch(dev); return 0; } static void object_update(void *data) { struct device *dev = data; struct impl *impl = dev->impl; const char *str; pw_log_debug("%p: device %p %08x/%08x", impl, dev, dev->obj->obj.changed, dev->obj->obj.avail); if (dev->obj->info && dev->obj->info->props && (str = spa_dict_lookup(dev->obj->info->props, PW_KEY_DEVICE_BUS)) != NULL && spa_streq(str, "bluetooth") && !impl->restore_bluetooth) return; if (dev->obj->obj.changed & SM_DEVICE_CHANGE_MASK_PARAMS) handle_profile(dev); } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; struct device *dev; const char *name; if (!spa_streq(object->type, PW_TYPE_INTERFACE_Device) || object->props == NULL || (name = pw_properties_get(object->props, PW_KEY_DEVICE_NAME)) == NULL) return; pw_log_debug("%p: add device '%d' %s", impl, object->id, name); dev = sm_object_add_data(object, SESSION_KEY, sizeof(struct device)); dev->obj = (struct sm_device*)object; dev->id = object->id; dev->impl = impl; dev->name = strdup(name); dev->key = spa_aprintf(PREFIX"%s", name); dev->active_profile = SPA_ID_INVALID; dev->best_profile = SPA_ID_INVALID; dev->obj->obj.mask |= SM_DEVICE_CHANGE_MASK_PARAMS; sm_object_add_listener(&dev->obj->obj, &dev->listener, &object_events, dev); } static void destroy_device(struct impl *impl, struct device *dev) { spa_hook_remove(&dev->listener); free(dev->name); free(dev->key); sm_object_remove_data((struct sm_object*)dev->obj, SESSION_KEY); } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; struct device *dev; if (!spa_streq(object->type, PW_TYPE_INTERFACE_Device)) return; pw_log_debug("%p: remove device '%d'", impl, object->id); if ((dev = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_device(impl, dev); } static void session_destroy(void *data) { struct impl *impl = data; remove_idle_timeout(impl); spa_hook_remove(&impl->listener); pw_properties_free(impl->properties); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .destroy = session_destroy, }; int sm_default_profile_start(struct sm_media_session *session) { struct impl *impl; int res; const char *str; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; impl->context = session->context; impl->properties = pw_properties_new(NULL, NULL); if (impl->properties == NULL) { free(impl); return -ENOMEM; } if ((str = pw_properties_get(session->props, "default-profile.restore-bluetooth")) != NULL) impl->restore_bluetooth = pw_properties_parse_bool(str); if ((res = sm_media_session_load_state(impl->session, SESSION_KEY, impl->properties)) < 0) pw_log_info("can't load "SESSION_KEY" state: %s", spa_strerror(res)); sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); return 0; } 07070100000068000081A40000000000000000000000016178A88C00006696000000000000000000000000000000000000002900000000media-session-0.4.1/src/default-routes.c/* PipeWire * * Copyright © 2020 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include <fcntl.h> #include <unistd.h> #include "config.h" #include <spa/utils/hook.h> #include <spa/utils/result.h> #include <spa/utils/json.h> #include <spa/utils/string.h> #include <spa/pod/parser.h> #include <spa/pod/builder.h> #include <spa/debug/pod.h> #include "pipewire/pipewire.h" #include "pipewire/extensions/metadata.h" #include "media-session.h" /** \page page_media_session_module_default_routes Media Session Module: Default Routes */ #define NAME "default-routes" #define SESSION_KEY "default-routes" #define PREFIX "default.route." #define SAVE_INTERVAL 1 PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct impl { struct timespec now; struct sm_media_session *session; struct spa_hook listener; struct pw_context *context; struct spa_source *idle_timeout; struct spa_hook meta_listener; struct pw_properties *to_restore; }; struct device { struct sm_device *obj; uint32_t id; struct impl *impl; char *name; struct spa_hook listener; uint32_t active_profile; uint32_t generation; struct pw_array route_info; }; static void remove_idle_timeout(struct impl *impl) { struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); int res; if (impl->idle_timeout) { if ((res = sm_media_session_save_state(impl->session, SESSION_KEY, impl->to_restore)) < 0) pw_log_error("can't save "SESSION_KEY" state: %s", spa_strerror(res)); pw_loop_destroy_source(main_loop, impl->idle_timeout); impl->idle_timeout = NULL; } } static void idle_timeout(void *data, uint64_t expirations) { struct impl *impl = data; pw_log_debug("%p: idle timeout", impl); remove_idle_timeout(impl); } static void add_idle_timeout(struct impl *impl) { struct timespec value; struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); if (impl->idle_timeout == NULL) impl->idle_timeout = pw_loop_add_timer(main_loop, idle_timeout, impl); value.tv_sec = SAVE_INTERVAL; value.tv_nsec = 0; pw_loop_update_timer(main_loop, impl->idle_timeout, &value, NULL, false); } static uint32_t channel_from_name(const char *name) { int i; for (i = 0; spa_type_audio_channel[i].name; i++) { if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) return spa_type_audio_channel[i].type; } return SPA_AUDIO_CHANNEL_UNKNOWN; } static const char *channel_to_name(uint32_t channel) { int i; for (i = 0; spa_type_audio_channel[i].name; i++) { if (spa_type_audio_channel[i].type == channel) return spa_debug_type_short_name(spa_type_audio_channel[i].name); } return "UNK"; } static uint32_t iec958Codec_from_name(const char *name) { int i; for (i = 0; spa_type_audio_iec958_codec[i].name; i++) { if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_iec958_codec[i].name))) return spa_type_audio_iec958_codec[i].type; } return SPA_AUDIO_IEC958_CODEC_UNKNOWN; } static const char *iec958Codec_to_name(uint32_t codec) { int i; for (i = 0; spa_type_audio_iec958_codec[i].name; i++) { if (spa_type_audio_iec958_codec[i].type == codec) return spa_debug_type_short_name(spa_type_audio_iec958_codec[i].name); } return "UNKNOWN"; } struct route_info { uint32_t index; uint32_t generation; enum spa_param_availability available; enum spa_param_availability prev_available; enum spa_direction direction; char name[64]; unsigned int save:1; unsigned int prev_active:1; unsigned int active:1; }; struct route { struct sm_param *p; uint32_t index; uint32_t device_id; enum spa_direction direction; const char *name; uint32_t priority; enum spa_param_availability available; struct spa_pod *props; struct spa_pod *profiles; bool save; }; #define ROUTE_INIT(__p) (struct route) { \ .p = (__p), \ .available = SPA_PARAM_AVAILABILITY_unknown, \ } static struct route_info *find_route_info(struct device *dev, const struct route *r) { struct route_info *i; pw_array_for_each(i, &dev->route_info) { if (i->index == r->index) return i; } i = pw_array_add(&dev->route_info, sizeof(*i)); if (i == NULL) return NULL; pw_log_info("device %d: new route %d '%s' found", dev->id, r->index, r->name); spa_zero(*i); i->index = r->index; snprintf(i->name, sizeof(i->name), "%s", r->name); i->direction = r->direction; i->generation = dev->generation; i->available = r->available; i->prev_available = r->available; return i; } static int parse_route(struct sm_param *p, struct route *r) { *r = ROUTE_INIT(p); return spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_ParamRoute, NULL, SPA_PARAM_ROUTE_index, SPA_POD_Int(&r->index), SPA_PARAM_ROUTE_direction, SPA_POD_Id(&r->direction), SPA_PARAM_ROUTE_device, SPA_POD_Int(&r->device_id), SPA_PARAM_ROUTE_name, SPA_POD_String(&r->name), SPA_PARAM_ROUTE_priority, SPA_POD_OPT_Int(&r->priority), SPA_PARAM_ROUTE_available, SPA_POD_OPT_Id(&r->available), SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&r->props), SPA_PARAM_ROUTE_save, SPA_POD_OPT_Bool(&r->save)); } static bool array_contains(const struct spa_pod *pod, uint32_t val) { uint32_t *vals, n_vals; uint32_t n; if (pod == NULL) return false; vals = spa_pod_get_array(pod, &n_vals); if (vals == NULL || n_vals == 0) return false; for (n = 0; n < n_vals; n++) if (vals[n] == val) return true; return false; } static int parse_enum_route(struct sm_param *p, uint32_t device_id, struct route *r) { struct spa_pod *devices = NULL; int res; *r = ROUTE_INIT(p); if ((res = spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_ParamRoute, NULL, SPA_PARAM_ROUTE_index, SPA_POD_Int(&r->index), SPA_PARAM_ROUTE_direction, SPA_POD_Id(&r->direction), SPA_PARAM_ROUTE_name, SPA_POD_String(&r->name), SPA_PARAM_ROUTE_priority, SPA_POD_OPT_Int(&r->priority), SPA_PARAM_ROUTE_available, SPA_POD_OPT_Id(&r->available), SPA_PARAM_ROUTE_devices, SPA_POD_OPT_Pod(&devices), SPA_PARAM_ROUTE_profiles, SPA_POD_OPT_Pod(&r->profiles))) < 0) return res; if (device_id != SPA_ID_INVALID && !array_contains(devices, device_id)) return -ENOENT; r->device_id = device_id; return 0; } static char *serialize_props(const struct device *dev, const struct spa_pod *param) { struct spa_pod_prop *prop; struct spa_pod_object *obj = (struct spa_pod_object *) param; bool comma = false; char *ptr; size_t size; FILE *f; f = open_memstream(&ptr, &size); fprintf(f, "{"); SPA_POD_OBJECT_FOREACH(obj, prop) { switch (prop->key) { case SPA_PROP_volume: { float val; if (spa_pod_get_float(&prop->value, &val) < 0) continue; fprintf(f, "%s \"volume\": %f", (comma ? "," : ""), val); break; } case SPA_PROP_mute: { bool b; if (spa_pod_get_bool(&prop->value, &b) < 0) continue; fprintf(f, "%s \"mute\": %s", (comma ? "," : ""), b ? "true" : "false"); break; } case SPA_PROP_channelVolumes: { uint32_t i, n_vals; float vals[SPA_AUDIO_MAX_CHANNELS]; n_vals = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, vals, SPA_AUDIO_MAX_CHANNELS); if (n_vals == 0) continue; fprintf(f, "%s \"volumes\": [", (comma ? "," : "")); for (i = 0; i < n_vals; i++) fprintf(f, "%s %f", (i == 0 ? "" : ","), vals[i]); fprintf(f, " ]"); break; } case SPA_PROP_channelMap: { uint32_t i, n_vals; uint32_t map[SPA_AUDIO_MAX_CHANNELS]; n_vals = spa_pod_copy_array(&prop->value, SPA_TYPE_Id, map, SPA_AUDIO_MAX_CHANNELS); if (n_vals == 0) continue; fprintf(f, "%s \"channels\": [", (comma ? "," : "")); for (i = 0; i < n_vals; i++) fprintf(f, "%s \"%s\"", (i == 0 ? "" : ","), channel_to_name(map[i])); fprintf(f, " ]"); break; } case SPA_PROP_latencyOffsetNsec: { int64_t delay; if (spa_pod_get_long(&prop->value, &delay) < 0) continue; fprintf(f, "%s \"latencyOffsetNsec\": %"PRIi64, (comma ? "," : ""), delay); break; } case SPA_PROP_iec958Codecs: { uint32_t i, codecs[64], n_codecs; n_codecs = spa_pod_copy_array(&prop->value, SPA_TYPE_Id, codecs, sizeof(codecs)); if (n_codecs == 0) continue; fprintf(f, "%s \"iec958Codecs\": [", (comma ? "," : "")); for (i = 0; i < n_codecs; i++) fprintf(f, "%s \"%s\"", (i == 0 ? "" : ","), iec958Codec_to_name(codecs[i])); fprintf(f, " ]"); break; } default: continue; } comma = true; } fprintf(f, " }"); fclose(f); return ptr; } static int restore_route_params(struct device *dev, const char *val, const struct route *r) { struct spa_json it[3]; char buf[1024], key[128]; const char *value; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); struct spa_pod_frame f[2]; struct spa_pod *param; spa_json_init(&it[0], val, strlen(val)); if (spa_json_enter_object(&it[0], &it[1]) <= 0) return -EINVAL; spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route); spa_pod_builder_add(&b, SPA_PARAM_ROUTE_index, SPA_POD_Int(r->index), SPA_PARAM_ROUTE_device, SPA_POD_Int(r->device_id), 0); spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_props, 0); spa_pod_builder_push_object(&b, &f[1], SPA_TYPE_OBJECT_Props, SPA_PARAM_Route); while (spa_json_get_string(&it[1], key, sizeof(key)-1) > 0) { if (spa_streq(key, "volume")) { float vol; if (spa_json_get_float(&it[1], &vol) <= 0) continue; spa_pod_builder_prop(&b, SPA_PROP_volume, 0); spa_pod_builder_float(&b, vol); } else if (spa_streq(key, "mute")) { bool mute; if (spa_json_get_bool(&it[1], &mute) <= 0) continue; spa_pod_builder_prop(&b, SPA_PROP_mute, 0); spa_pod_builder_bool(&b, mute); } else if (spa_streq(key, "volumes")) { uint32_t n_vols; float vols[SPA_AUDIO_MAX_CHANNELS]; if (spa_json_enter_array(&it[1], &it[2]) <= 0) continue; for (n_vols = 0; n_vols < SPA_AUDIO_MAX_CHANNELS; n_vols++) { if (spa_json_get_float(&it[2], &vols[n_vols]) <= 0) break; } if (n_vols == 0) continue; spa_pod_builder_prop(&b, SPA_PROP_channelVolumes, 0); spa_pod_builder_array(&b, sizeof(float), SPA_TYPE_Float, n_vols, vols); } else if (spa_streq(key, "channels")) { uint32_t n_ch; uint32_t map[SPA_AUDIO_MAX_CHANNELS]; if (spa_json_enter_array(&it[1], &it[2]) <= 0) continue; for (n_ch = 0; n_ch < SPA_AUDIO_MAX_CHANNELS; n_ch++) { char chname[16]; if (spa_json_get_string(&it[2], chname, sizeof(chname)) <= 0) break; map[n_ch] = channel_from_name(chname); } if (n_ch == 0) continue; spa_pod_builder_prop(&b, SPA_PROP_channelMap, 0); spa_pod_builder_array(&b, sizeof(uint32_t), SPA_TYPE_Id, n_ch, map); } else if (spa_streq(key, "latencyOffsetNsec")) { float delay; if (spa_json_get_float(&it[1], &delay) <= 0) continue; spa_pod_builder_prop(&b, SPA_PROP_latencyOffsetNsec, 0); spa_pod_builder_long(&b, (int64_t)SPA_CLAMP(delay, INT64_MIN, INT64_MAX)); } else if (spa_streq(key, "iec958Codecs")) { uint32_t n_codecs; uint32_t codecs[64]; if (spa_json_enter_array(&it[1], &it[2]) <= 0) continue; for (n_codecs = 0; n_codecs < 64; n_codecs++) { char name[16]; if (spa_json_get_string(&it[2], name, sizeof(name)) <= 0) break; codecs[n_codecs] = iec958Codec_from_name(name); } if (n_codecs == 0) continue; spa_pod_builder_prop(&b, SPA_PROP_iec958Codecs, 0); spa_pod_builder_array(&b, sizeof(uint32_t), SPA_TYPE_Id, n_codecs, codecs); } else { if (spa_json_next(&it[1], &value) <= 0) break; } } spa_pod_builder_pop(&b, &f[1]); spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_save, 0); spa_pod_builder_bool(&b, r->save); param = spa_pod_builder_pop(&b, &f[0]); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, param); pw_device_set_param((struct pw_node*)dev->obj->obj.proxy, SPA_PARAM_Route, 0, param); sm_media_session_schedule_rescan(dev->impl->session); return 0; } struct profile { uint32_t index; const char *name; struct spa_pod *classes; }; static int parse_profile(const struct sm_param *p, struct profile *pr) { int res; spa_zero(*pr); if ((res = spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_ParamProfile, NULL, SPA_PARAM_PROFILE_index, SPA_POD_Int(&pr->index), SPA_PARAM_PROFILE_name, SPA_POD_String(&pr->name), SPA_PARAM_PROFILE_classes, SPA_POD_OPT_Pod(&pr->classes))) < 0) return res; return 0; } static int find_current_profile(const struct device *dev, struct profile *pr) { struct sm_param *p; spa_list_for_each(p, &dev->obj->param_list, link) { if (p->id == SPA_PARAM_Profile && parse_profile(p, pr) >= 0) return 0; } return -ENOENT; } static int restore_route(struct device *dev, const struct route *r) { struct impl *impl = dev->impl; char key[1024]; const char *val; struct route_info *ri; if ((ri = find_route_info(dev, r)) == NULL) return -errno; snprintf(key, sizeof(key), PREFIX"%s:%s:%s", dev->name, r->direction == SPA_DIRECTION_INPUT ? "input" : "output", r->name); val = pw_properties_get(impl->to_restore, key); if (val == NULL) val = "{ \"volumes\": [ 0.4 ], \"mute\": false }"; pw_log_info("device %d: restore route %d '%s' to %s", dev->id, r->index, key, val); restore_route_params(dev, val, r); ri->prev_active = true; ri->active = true; ri->generation = dev->generation; ri->save = r->save; return 0; } static int save_route(const struct device *dev, const struct route *r) { struct impl *impl = dev->impl; char key[1024], *val; if (r->props == NULL) return -EINVAL; snprintf(key, sizeof(key), PREFIX"%s:%s:%s", dev->name, r->direction == SPA_DIRECTION_INPUT ? "input" : "output", r->name); val = serialize_props(dev, r->props); if (pw_properties_set(impl->to_restore, key, val)) { pw_log_info("device %d: route properties changed %s %s", dev->id, key, val); add_idle_timeout(impl); } free(val); return 0; } static char *serialize_routes(const struct device *dev) { char *ptr; size_t size; FILE *f; const struct route_info *ri; int count = 0; f = open_memstream(&ptr, &size); fprintf(f, "["); pw_array_for_each(ri, &dev->route_info) { if (ri->save) { fprintf(f, "%s \"%s\"", count++ == 0 ? "" : ",", ri->name); } } fprintf(f, " ]"); fclose(f); return ptr; } static int save_profile(struct device *dev, const char *profile_name) { struct impl *impl = dev->impl; char key[1024], *val; if (pw_array_get_len(&dev->route_info, struct route_info) == 0) return 0; snprintf(key, sizeof(key), PREFIX"%s:profile:%s", dev->name, profile_name); val = serialize_routes(dev); if (pw_properties_set(impl->to_restore, key, val)) { pw_log_info("device %d: profile %s routes changed %s %s", dev->id, profile_name, key, val); add_idle_timeout(impl); } else { pw_log_info("device %d: profile %s unchanged (%s)", dev->id, profile_name, val); } free(val); return 0; } static int find_best_route(struct device *dev, uint32_t device_id, struct route *r) { struct sm_param *p; struct route best, best_avail, best_unk; spa_zero(best_avail); spa_zero(best_unk); spa_list_for_each(p, &dev->obj->param_list, link) { struct route t; if (p->id != SPA_PARAM_EnumRoute || parse_enum_route(p, device_id, &t) < 0) continue; if (t.available == SPA_PARAM_AVAILABILITY_yes || t.available == SPA_PARAM_AVAILABILITY_unknown) { struct route_info *ri; if ((ri = find_route_info(dev, &t)) && ri->direction == SPA_DIRECTION_OUTPUT && ri->available != ri->prev_available) { /* If route availability changed, that means a user just * plugged in something like headphones, and they probably * expect to hear sound from it. Switch to it immediately. * * TODO: switch INPUT ports without source and the input * ports their source->active_port is part of a group of * ports (see module-switch-on-port-available.c in PulseAudio). */ best_avail = t; ri->save = true; break; } else if (t.available == SPA_PARAM_AVAILABILITY_yes) { if (best_avail.name == NULL || t.priority > best_avail.priority) best_avail = t; } else { // SPA_PARAM_AVAILABILITY_unknown if (best_unk.name == NULL || t.priority > best_unk.priority) best_unk = t; } } } best = best_avail; if (best.name == NULL) best = best_unk; if (best.name == NULL) return -ENOENT; *r = best; return 0; } static int find_route(const struct device *dev, uint32_t device_id, const char *name, struct route *r) { struct sm_param *p; spa_list_for_each(p, &dev->obj->param_list, link) { if (p->id != SPA_PARAM_EnumRoute || parse_enum_route(p, device_id, r) < 0) continue; if (!spa_streq(r->name, name)) continue; return 0; } return -ENOENT; } static int find_saved_route(struct device *dev, const char *val, uint32_t device_id, struct route *r) { struct spa_json it[2]; char key[128]; if (val == NULL) return -ENOENT; spa_json_init(&it[0], val, strlen(val)); if (spa_json_enter_array(&it[0], &it[1]) <= 0) return -EINVAL; while (spa_json_get_string(&it[1], key, sizeof(key)-1) > 0) { if (find_route(dev, device_id, key, r) >= 0) return 0; } return -ENOENT; } static int restore_device_route(struct device *dev, const char *val, uint32_t device_id, bool restore) { int res = -ENOENT; struct route t; pw_log_info("device %d: restoring device %u", dev->id, device_id); if (restore) { res = find_saved_route(dev, val, device_id, &t); if (res >= 0) { /* we found a saved route */ if (t.available == SPA_PARAM_AVAILABILITY_no) { pw_log_info("device %d: saved route '%s' not available", dev->id, t.name); /* not available, try to find next best port */ res = -ENOENT; } else { pw_log_info("device %d: found saved route '%s'", dev->id, t.name); /* make sure we save it again */ t.save = true; } } } if (res < 0) { /* we could not find a saved route, try to find a new best */ res = find_best_route(dev, device_id, &t); if (res < 0) { pw_log_info("device %d: can't find best route", dev->id); } else { pw_log_info("device %d: found best route '%s'", dev->id, t.name); } } if (res >= 0) restore_route(dev, &t); return res; } static int reconfigure_profile(struct device *dev, struct profile *pr, bool restore) { struct impl *impl = dev->impl; char key[1024]; const char *json; pw_log_info("device %s: restore routes for profile '%s'", dev->name, pr->name); dev->active_profile = pr->index; snprintf(key, sizeof(key), PREFIX"%s:profile:%s", dev->name, pr->name); json = pw_properties_get(impl->to_restore, key); if (pr->classes != NULL) { struct spa_pod *iter; SPA_POD_STRUCT_FOREACH(pr->classes, iter) { struct spa_pod_parser prs; struct spa_pod_frame f[1]; struct spa_pod *val; char *key; spa_pod_parser_pod(&prs, iter); if (spa_pod_parser_push_struct(&prs, &f[0]) < 0) continue; while (spa_pod_parser_get(&prs, SPA_POD_String(&key), SPA_POD_Pod(&val), NULL) >= 0) { if (key == NULL || val == NULL) break; if (spa_streq(key, "card.profile.devices")) { uint32_t *devices, n_devices, i; devices = spa_pod_get_array(val, &n_devices); if (devices == NULL || n_devices == 0) continue; for (i = 0; i < n_devices; i++) restore_device_route(dev, json, devices[i], restore); } } spa_pod_parser_pop(&prs, &f[0]); } } return 0; } static void prune_route_info(struct device *dev) { struct route_info *i; for (i = pw_array_first(&dev->route_info); pw_array_check(&dev->route_info, i);) { if (i->generation != dev->generation) { pw_log_info("device %d: route '%s' unused", dev->id, i->name); pw_array_remove(&dev->route_info, i); } else i++; } } static int handle_route(struct device *dev, const struct route *r) { struct route_info *ri; pw_log_info("device %d: port '%s'", dev->id, r->name); if ((ri = find_route_info(dev, r)) == NULL) return -errno; ri->active = true; ri->save = r->save; if (!ri->prev_active) { /* a new port has been found, restore the volume and make sure we * save this as a preferred port */ pw_log_info("device %d: new active port found '%s'", dev->id, r->name); restore_route(dev, r); } else if (r->props && r->save) { /* just save port properties */ save_route(dev, r); } return 0; } static int handle_device(struct device *dev) { struct profile pr; struct sm_param *p; bool route_changed = false; dev->generation++; if (find_current_profile(dev, &pr) < 0) pr.index = SPA_ID_INVALID; /* first look at all routes and update */ spa_list_for_each(p, &dev->obj->param_list, link) { struct route r; struct route_info *ri; if (p->id != SPA_PARAM_EnumRoute || parse_enum_route(p, SPA_ID_INVALID, &r) < 0) continue; if ((ri = find_route_info(dev, &r)) == NULL) continue; ri->prev_available = ri->available; if (ri->available != r.available) { pw_log_info("device %d: route %s available changed %d -> %d", dev->id, r.name, ri->available, r.available); ri->available = r.available; if (array_contains(r.profiles, pr.index)) route_changed = true; } ri->generation = dev->generation; ri->prev_active = ri->active; ri->active = false; ri->save = false; } /* then check for changes in the active ports */ spa_list_for_each(p, &dev->obj->param_list, link) { struct route r; if (p->id != SPA_PARAM_Route || parse_route(p, &r) < 0) continue; handle_route(dev, &r); } prune_route_info(dev); if (pr.index != SPA_ID_INVALID) { bool restore = dev->active_profile != pr.index; if (restore || route_changed) reconfigure_profile(dev, &pr, restore); save_profile(dev, pr.name); } return 0; } static void object_update(void *data) { struct device *dev = data; struct impl *impl = dev->impl; pw_log_debug("%p: device %p %08x/%08x", impl, dev, dev->obj->obj.changed, dev->obj->obj.avail); if (dev->obj->obj.changed & SM_DEVICE_CHANGE_MASK_PARAMS) handle_device(dev); } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; struct device *dev; const char *name; if (!spa_streq(object->type, PW_TYPE_INTERFACE_Device) || object->props == NULL || (name = pw_properties_get(object->props, PW_KEY_DEVICE_NAME)) == NULL) return; pw_log_debug("%p: add device '%d' %s", impl, object->id, name); dev = sm_object_add_data(object, SESSION_KEY, sizeof(struct device)); dev->obj = (struct sm_device*)object; dev->id = object->id; dev->impl = impl; dev->name = strdup(name); dev->active_profile = SPA_ID_INVALID; dev->generation = 0; pw_array_init(&dev->route_info, sizeof(struct route_info) * 16); dev->obj->obj.mask |= SM_DEVICE_CHANGE_MASK_PARAMS; sm_object_add_listener(&dev->obj->obj, &dev->listener, &object_events, dev); } static void destroy_device(struct impl *impl, struct device *dev) { spa_hook_remove(&dev->listener); pw_array_clear(&dev->route_info); free(dev->name); sm_object_remove_data((struct sm_object*)dev->obj, SESSION_KEY); } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; struct device *dev; if (!spa_streq(object->type, PW_TYPE_INTERFACE_Device)) return; pw_log_debug("%p: remove device '%d'", impl, object->id); if ((dev = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_device(impl, dev); } static void session_destroy(void *data) { struct impl *impl = data; remove_idle_timeout(impl); spa_hook_remove(&impl->listener); pw_properties_free(impl->to_restore); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .destroy = session_destroy, }; int sm_default_routes_start(struct sm_media_session *session) { struct impl *impl; int res; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; impl->context = session->context; impl->to_restore = pw_properties_new(NULL, NULL); if (impl->to_restore == NULL) { res = -errno; goto exit_free; } if ((res = sm_media_session_load_state(impl->session, SESSION_KEY, impl->to_restore)) < 0) pw_log_info("can't load "SESSION_KEY" state: %s", spa_strerror(res)); sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); return 0; exit_free: free(impl); return res; } 07070100000069000081A40000000000000000000000016178A88C0000303D000000000000000000000000000000000000002C00000000media-session-0.4.1/src/libcamera-monitor.c/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <spa/monitor/device.h> #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/utils/names.h> #include <spa/utils/result.h> #include <spa/utils/string.h> #include <spa/param/props.h> #include <spa/debug/dict.h> #include <spa/pod/builder.h> #include "pipewire/pipewire.h" #include "media-session.h" /** \page page_media_session_module_libcamera_monitor Media Session Module: libCamera Monitor */ #define NAME "libcamera-monitor" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct device; struct node { struct impl *impl; struct device *device; struct spa_list link; uint32_t id; struct pw_properties *props; struct pw_proxy *proxy; struct spa_node *node; }; struct device { struct impl *impl; struct spa_list link; uint32_t id; uint32_t device_id; int priority; int profile; struct pw_properties *props; struct spa_handle *handle; struct spa_device *device; struct spa_hook device_listener; struct sm_device *sdevice; struct spa_hook listener; unsigned int appeared:1; struct spa_list node_list; }; struct impl { struct sm_media_session *session; struct spa_hook session_listener; struct spa_handle *handle; struct spa_device *monitor; struct spa_hook listener; struct spa_list device_list; }; static struct node *libcamera_find_node(struct device *dev, uint32_t id) { struct node *node; spa_list_for_each(node, &dev->node_list, link) { if (node->id == id) return node; } return NULL; } static void libcamera_update_node(struct device *dev, struct node *node, const struct spa_device_object_info *info) { pw_log_debug("update node %u", node->id); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); pw_properties_update(node->props, info->props); } static struct node *libcamera_create_node(struct device *dev, uint32_t id, const struct spa_device_object_info *info) { struct node *node; struct impl *impl = dev->impl; int res; const char *str; pw_log_debug("new node %u", id); if (!spa_streq(info->type, SPA_TYPE_INTERFACE_Node)) { errno = EINVAL; return NULL; } node = calloc(1, sizeof(*node)); if (node == NULL) { res = -errno; goto exit; } node->props = pw_properties_new_dict(info->props); pw_properties_setf(node->props, PW_KEY_DEVICE_ID, "%d", dev->device_id); str = pw_properties_get(dev->props, SPA_KEY_DEVICE_NAME); if (str == NULL) str = pw_properties_get(dev->props, SPA_KEY_DEVICE_NICK); if (str == NULL) str = pw_properties_get(dev->props, SPA_KEY_DEVICE_ALIAS); if (str == NULL) str = "libcamera-device"; pw_properties_setf(node->props, PW_KEY_NODE_NAME, "%s.%s", info->factory_name, str); str = pw_properties_get(dev->props, SPA_KEY_DEVICE_DESCRIPTION); if (str == NULL) str = "libcamera-device"; pw_properties_set(node->props, PW_KEY_NODE_DESCRIPTION, str); pw_properties_set(node->props, PW_KEY_FACTORY_NAME, info->factory_name); node->impl = impl; node->device = dev; node->id = id; node->proxy = sm_media_session_create_object(impl->session, "spa-node-factory", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, &node->props->dict, 0); if (node->proxy == NULL) { res = -errno; goto clean_node; } spa_list_append(&dev->node_list, &node->link); return node; clean_node: pw_properties_free(node->props); free(node); exit: errno = -res; return NULL; } static void libcamera_remove_node(struct device *dev, struct node *node) { pw_log_debug("remove node %u", node->id); spa_list_remove(&node->link); pw_proxy_destroy(node->proxy); pw_properties_free(node->props); free(node); } static void libcamera_device_info(void *data, const struct spa_device_info *info) { struct device *dev = data; if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); pw_properties_update(dev->props, info->props); } static void libcamera_device_object_info(void *data, uint32_t id, const struct spa_device_object_info *info) { struct device *dev = data; struct node *node; node = libcamera_find_node(dev, id); if (info == NULL) { if (node == NULL) { pw_log_warn("device %p: unknown node %u", dev, id); return; } libcamera_remove_node(dev, node); } else if (node == NULL) { libcamera_create_node(dev, id, info); } else { libcamera_update_node(dev, node, info); } } static const struct spa_device_events libcamera_device_events = { SPA_VERSION_DEVICE_EVENTS, .info = libcamera_device_info, .object_info = libcamera_device_object_info }; static struct device *libcamera_find_device(struct impl *impl, uint32_t id) { struct device *dev; spa_list_for_each(dev, &impl->device_list, link) { if (dev->id == id) return dev; } return NULL; } static void libcamera_update_device(struct impl *impl, struct device *dev, const struct spa_device_object_info *info) { pw_log_debug("update device %u", dev->id); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); pw_properties_update(dev->props, info->props); } static int libcamera_update_device_props(struct device *dev) { struct pw_properties *p = dev->props; const char *s, *d; char temp[32]; if ((s = pw_properties_get(p, SPA_KEY_DEVICE_NAME)) == NULL) { if ((s = pw_properties_get(p, SPA_KEY_DEVICE_BUS_ID)) == NULL) { if ((s = pw_properties_get(p, SPA_KEY_DEVICE_BUS_PATH)) == NULL) { snprintf(temp, sizeof(temp), "%d", dev->id); s = temp; } } } pw_properties_setf(p, PW_KEY_DEVICE_NAME, "libcamera_device.%s", s); if (pw_properties_get(p, PW_KEY_DEVICE_DESCRIPTION) == NULL) { d = pw_properties_get(p, PW_KEY_DEVICE_PRODUCT_NAME); if (!d) d = "Unknown device"; pw_properties_set(p, PW_KEY_DEVICE_DESCRIPTION, d); } return 0; } static void set_profile(struct device *device, int index) { char buf[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); pw_log_debug("%p: set profile %d id:%d", device, index, device->device_id); device->profile = index; if (device->device_id != 0) { spa_device_set_param(device->device, SPA_PARAM_Profile, 0, spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile, SPA_PARAM_PROFILE_index, SPA_POD_Int(index))); } } static void device_destroy(void *data) { struct device *device = data; struct node *node; pw_log_debug("device %p destroy", device); spa_list_consume(node, &device->node_list, link) libcamera_remove_node(device, node); } static void device_free(void *data) { struct device *dev = data; pw_log_debug("remove device %u", dev->id); spa_list_remove(&dev->link); if (dev->appeared) spa_hook_remove(&dev->device_listener); sm_object_discard(&dev->sdevice->obj); spa_hook_remove(&dev->listener); pw_unload_spa_handle(dev->handle); pw_properties_free(dev->props); free(dev); } static void device_update(void *data) { struct device *device = data; pw_log_debug("device %p appeared %d %d", device, device->appeared, device->profile); if (device->appeared) return; device->device_id = device->sdevice->obj.id; device->appeared = true; spa_device_add_listener(device->device, &device->device_listener, &libcamera_device_events, device); set_profile(device, 1); sm_object_sync_update(&device->sdevice->obj); } static const struct sm_object_events device_events = { SM_VERSION_OBJECT_EVENTS, .destroy = device_destroy, .free = device_free, .update = device_update, }; static struct device *libcamera_create_device(struct impl *impl, uint32_t id, const struct spa_device_object_info *info) { struct pw_context *context = impl->session->context; struct device *dev; struct spa_handle *handle; int res; void *iface; pw_log_debug("new device %u", id); if (!spa_streq(info->type, SPA_TYPE_INTERFACE_Device)) { errno = EINVAL; return NULL; } handle = pw_context_load_spa_handle(context, info->factory_name, info->props); if (handle == NULL) { res = -errno; pw_log_error("can't make factory instance: %m"); goto exit; } if ((res = spa_handle_get_interface(handle, info->type, &iface)) < 0) { pw_log_error("can't get %s interface: %s", info->type, spa_strerror(res)); goto unload_handle; } dev = calloc(1, sizeof(*dev)); if (dev == NULL) { res = -errno; goto unload_handle; } dev->impl = impl; dev->id = id; dev->handle = handle; dev->device = iface; dev->props = pw_properties_new_dict(info->props); libcamera_update_device_props(dev); dev->sdevice = sm_media_session_export_device(impl->session, &dev->props->dict, dev->device); if (dev->sdevice == NULL) { res = -errno; goto clean_device; } pw_log_debug("got object %p", &dev->sdevice->obj); sm_object_add_listener(&dev->sdevice->obj, &dev->listener, &device_events, dev); spa_list_init(&dev->node_list); spa_list_append(&impl->device_list, &dev->link); return dev; clean_device: free(dev); unload_handle: pw_unload_spa_handle(handle); exit: errno = -res; return NULL; } static void libcamera_remove_device(struct impl *impl, struct device *dev) { sm_object_destroy(&dev->sdevice->obj); } static void libcamera_udev_object_info(void *data, uint32_t id, const struct spa_device_object_info *info) { struct impl *impl = data; struct device *dev = NULL; dev = libcamera_find_device(impl, id); if (info == NULL) { if (dev == NULL) return; libcamera_remove_device(impl, dev); } else if (dev == NULL) { if (libcamera_create_device(impl, id, info) == NULL) return; } else { libcamera_update_device(impl, dev, info); } } static const struct spa_device_events libcamera_udev_callbacks = { SPA_VERSION_DEVICE_EVENTS, .object_info = libcamera_udev_object_info, }; static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->session_listener); spa_hook_remove(&impl->listener); pw_unload_spa_handle(impl->handle); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .destroy = session_destroy, }; int sm_libcamera_monitor_start(struct sm_media_session *sess) { struct pw_context *context = sess->context; struct impl *impl; int res; void *iface; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = sess; impl->handle = pw_context_load_spa_handle(context, SPA_NAME_API_LIBCAMERA_ENUM_CLIENT, NULL); if (impl->handle == NULL) { res = -errno; goto out_free; } if ((res = spa_handle_get_interface(impl->handle, SPA_TYPE_INTERFACE_Device, &iface)) < 0) { pw_log_error("can't get MONITOR interface: %d", res); goto out_unload; } impl->monitor = iface; spa_list_init(&impl->device_list); spa_device_add_listener(impl->monitor, &impl->listener, &libcamera_udev_callbacks, impl); sm_media_session_add_listener(sess, &impl->session_listener, &session_events, impl); return 0; out_unload: pw_unload_spa_handle(impl->handle); out_free: free(impl); return res; } 0707010000006A000081A40000000000000000000000016178A88C00000FCE000000000000000000000000000000000000002100000000media-session-0.4.1/src/logind.c/* PipeWire * * Copyright © 2021 Pauli Virtanen * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ /* * Monitor systemd-logind events for changes in session/seat status, and keep session * manager up-to-date on whether the current session is active. */ #include "config.h" #include <sys/types.h> #include <unistd.h> #include <systemd/sd-login.h> #include <spa/utils/result.h> #include <spa/utils/string.h> #include "pipewire/pipewire.h" #include "media-session.h" /** \page page_media_session_module_logind Media Session Module: Logind * * The logind module uses systemd logind to keep track of the user's session * and updates the media session's seat state accordingly. * * The session state may be used by other modules, e.g. the \ref * page_media_session_module_bluez_monitor module enables/disables * Bluetooth whenever the session changes between active and inactive. */ #define NAME "logind" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct impl { struct sm_media_session *session; struct spa_hook listener; struct pw_context *context; sd_login_monitor *monitor; struct spa_source source; }; static void update_seat_active(struct impl *impl) { char *state; bool active; if (sd_uid_get_state(getuid(), &state) < 0) return; active = spa_streq(state, "active"); free(state); sm_media_session_seat_active_changed(impl->session, active); } static void monitor_event(struct spa_source *source) { struct impl *impl = source->data; sd_login_monitor_flush(impl->monitor); update_seat_active(impl); } static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->listener); if (impl->monitor) { struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); pw_loop_remove_source(main_loop, &impl->source); sd_login_monitor_unref(impl->monitor); impl->monitor = NULL; } free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .destroy = session_destroy, }; int sm_logind_start(struct sm_media_session *session) { struct impl *impl; struct pw_loop *main_loop; int res; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; impl->context = session->context; if ((res = sd_login_monitor_new(NULL, &impl->monitor)) < 0) goto fail; main_loop = pw_context_get_main_loop(impl->context); impl->source.data = impl; impl->source.fd = sd_login_monitor_get_fd(impl->monitor); impl->source.func = monitor_event; impl->source.mask = sd_login_monitor_get_events(impl->monitor); impl->source.rmask = 0; pw_loop_add_source(main_loop, &impl->source); sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); update_seat_active(impl); return 0; fail: pw_log_error(": failed to start systemd logind monitor: %d (%s)", res, spa_strerror(res)); free(impl); return res; } 0707010000006B000081A40000000000000000000000016178A88C000010AB000000000000000000000000000000000000002600000000media-session-0.4.1/src/match-rules.c/* PipeWire * * Copyright © 2020 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include <regex.h> #include "config.h" #include <spa/utils/json.h> #include <spa/utils/string.h> #include <pipewire/pipewire.h> #include "media-session.h" PW_LOG_TOPIC_EXTERN(ms_topic); #define PW_LOG_TOPIC_DEFAULT ms_topic static bool find_match(struct spa_json *arr, struct pw_properties *props) { struct spa_json match_obj; while (spa_json_enter_object(arr, &match_obj) > 0) { char key[256], val[1024]; const char *str, *value; int match = 0, fail = 0; int len; while (spa_json_get_string(&match_obj, key, sizeof(key)-1) > 0) { bool success = false; if ((len = spa_json_next(&match_obj, &value)) <= 0) break; str = pw_properties_get(props, key); if (spa_json_is_null(value, len)) { success = str == NULL; } else { spa_json_parse_string(value, SPA_MIN(len, 1023), val); value = val; len = strlen(val); } if (str != NULL) { if (value[0] == '~') { regex_t preg; if (regcomp(&preg, value+1, REG_EXTENDED | REG_NOSUB) == 0) { if (regexec(&preg, str, 0, NULL, 0) == 0) success = true; regfree(&preg); } } else if (strncmp(str, value, len) == 0 && strlen(str) == (size_t)len) { success = true; } } if (success) { match++; pw_log_debug("'%s' match '%s' < > '%.*s'", key, str, len, value); } else fail++; } if (match > 0 && fail == 0) return true; } return false; } int sm_media_session_match_rules(const char *rules, size_t size, struct pw_properties *props) { const char *val; struct spa_json actions; struct spa_json it_rules; /* the rules = [] array */ struct spa_json it_rules_obj; /* one object within that array */ struct spa_json it_element; /* key/value element within that object */ spa_json_init(&it_rules, rules, size); if (spa_json_enter_array(&it_rules, &it_rules_obj) < 0) return 0; while (spa_json_enter_object(&it_rules_obj, &it_element) > 0) { char key[64]; bool have_match = false, have_actions = false; while (spa_json_get_string(&it_element, key, sizeof(key)-1) > 0) { if (spa_streq(key, "matches")) { struct spa_json it_matches_array; if (spa_json_enter_array(&it_element, &it_matches_array) < 0) break; have_match = find_match(&it_matches_array, props); } else if (spa_streq(key, "actions")) { if (spa_json_enter_object(&it_element, &actions) > 0) have_actions = true; } else if (spa_json_next(&it_element, &val) <= 0) break; } if (!have_match || !have_actions) continue; while (spa_json_get_string(&actions, key, sizeof(key)-1) > 0) { int len; pw_log_debug("action %s", key); if (spa_streq(key, "update-props")) { if ((len = spa_json_next(&actions, &val)) <= 0) continue; if (!spa_json_is_object(val, len)) continue; len = spa_json_container_len(&actions, val, len); pw_properties_update_string(props, val, len); } else if (spa_json_next(&actions, &val) <= 0) break; } } return 1; } 0707010000006C000081A40000000000000000000000016178A88C00011BC0000000000000000000000000000000000000002800000000media-session-0.4.1/src/media-session.c/* PipeWire * * Copyright © 2018 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include "config.h" #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <getopt.h> #include <time.h> #include <unistd.h> #include <limits.h> #include <fcntl.h> #include <signal.h> #include <sys/stat.h> #include <sys/mman.h> #include <sys/types.h> #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/utils/result.h> #include <spa/utils/json.h> #include <spa/utils/string.h> #include <spa/param/audio/format-utils.h> #include <spa/param/props.h> #include <spa/debug/pod.h> #include <spa/support/dbus.h> #include <spa/monitor/device.h> #include "pipewire/pipewire.h" #include "pipewire/conf.h" #include "pipewire/extensions/session-manager.h" #include "pipewire/extensions/client-node.h" #include <dbus/dbus.h> #include "media-session.h" #define NAME "media-session" #define SESSION_PREFIX "media-session.d" #define SESSION_CONF "media-session.conf" PW_LOG_TOPIC(ms_topic, "ms.core"); #define PW_LOG_TOPIC_DEFAULT ms_topic #define sm_object_emit(o,m,v,...) spa_hook_list_call(&(o)->hooks, struct sm_object_events, m, v, ##__VA_ARGS__) #define sm_object_emit_update(s) sm_object_emit(s, update, 0) #define sm_object_emit_destroy(s) sm_object_emit(s, destroy, 0) #define sm_object_emit_free(s) sm_object_emit(s, free, 0) #define sm_media_session_emit(s,m,v,...) spa_hook_list_call(&(s)->hooks, struct sm_media_session_events, m, v, ##__VA_ARGS__) #define sm_media_session_emit_info(s,i) sm_media_session_emit(s, info, 0, i) #define sm_media_session_emit_create(s,obj) sm_media_session_emit(s, create, 0, obj) #define sm_media_session_emit_remove(s,obj) sm_media_session_emit(s, remove, 0, obj) #define sm_media_session_emit_rescan(s,seq) sm_media_session_emit(s, rescan, 0, seq) #define sm_media_session_emit_shutdown(s) sm_media_session_emit(s, shutdown, 0) #define sm_media_session_emit_destroy(s) sm_media_session_emit(s, destroy, 0) #define sm_media_session_emit_seat_active(s,...) sm_media_session_emit(s, seat_active, 0, __VA_ARGS__) #define sm_media_session_emit_dbus_disconnected(s) sm_media_session_emit(s, dbus_disconnected, 0) int sm_access_flatpak_start(struct sm_media_session *sess); int sm_access_portal_start(struct sm_media_session *sess); int sm_default_nodes_start(struct sm_media_session *sess); int sm_default_profile_start(struct sm_media_session *sess); int sm_default_routes_start(struct sm_media_session *sess); int sm_restore_stream_start(struct sm_media_session *sess); int sm_streams_follow_default_start(struct sm_media_session *sess); int sm_alsa_no_dsp_start(struct sm_media_session *sess); int sm_alsa_midi_start(struct sm_media_session *sess); int sm_v4l2_monitor_start(struct sm_media_session *sess); int sm_libcamera_monitor_start(struct sm_media_session *sess); int sm_bluez5_monitor_start(struct sm_media_session *sess); int sm_bluez5_autoswitch_start(struct sm_media_session *sess); int sm_alsa_monitor_start(struct sm_media_session *sess); int sm_suspend_node_start(struct sm_media_session *sess); #ifdef HAVE_SYSTEMD int sm_logind_start(struct sm_media_session *sess); #endif int sm_policy_node_start(struct sm_media_session *sess); int sm_session_manager_start(struct sm_media_session *sess); /** user data to add to an object */ struct data { struct spa_list link; const char *id; size_t size; }; struct param { struct sm_param this; }; struct sync { struct spa_list link; int seq; void (*callback) (void *data); void *data; }; struct impl { struct sm_media_session this; const char *config_dir; struct pw_properties *conf; struct pw_properties *modules; struct pw_main_loop *loop; struct spa_dbus *dbus; struct spa_hook dbus_connection_listener; struct pw_core *monitor_core; struct spa_hook monitor_listener; int monitor_seq; struct pw_core *policy_core; struct spa_hook policy_listener; struct spa_hook proxy_policy_listener; struct pw_registry *registry; struct spa_hook registry_listener; struct pw_registry *monitor_registry; struct spa_hook monitor_registry_listener; struct pw_map globals; struct spa_list object_list; /**< all sm_objects */ struct spa_list registry_event_list; /**< pending registry events */ struct spa_hook_list hooks; struct spa_list endpoint_link_list; /** list of struct endpoint_link */ struct pw_map endpoint_links; /** map of endpoint_link */ struct spa_list link_list; /** list of struct link */ struct spa_list sync_list; /** list of struct sync */ int rescan_seq; int last_seq; unsigned int scanning:1; unsigned int rescan_pending:1; unsigned int seat_active:1; }; struct endpoint_link { uint32_t id; struct pw_endpoint_link_info info; struct impl *impl; struct spa_list link; /**< link in struct impl endpoint_link_list */ struct spa_list link_list; /**< list of struct link */ }; struct link { struct pw_proxy *proxy; /**< proxy for link */ struct spa_hook listener; /**< proxy listener */ uint32_t output_node; uint32_t output_port; uint32_t input_node; uint32_t input_port; struct endpoint_link *endpoint_link; struct spa_list link; /**< link in struct endpoint_link link_list or * struct impl link_list */ }; struct object_info { const char *type; uint32_t version; const void *events; size_t size; int (*init) (void *object); void (*destroy) (void *object); }; struct registry_event { uint32_t id; uint32_t permissions; const char *type; uint32_t version; const struct spa_dict *props; struct pw_proxy *proxy; int seq; struct pw_properties *props_store; struct spa_list link; unsigned int monitor:1; unsigned int allocated:1; }; static void add_object(struct impl *impl, struct sm_object *obj, uint32_t id) { size_t size = pw_map_get_size(&impl->globals); obj->id = id; pw_log_debug("add global '%u' %p monitor:%d", obj->id, obj, obj->monitor_global); while (obj->id > size) pw_map_insert_at(&impl->globals, size++, NULL); pw_map_insert_at(&impl->globals, obj->id, obj); sm_media_session_emit_create(impl, obj); } static void remove_object(struct impl *impl, struct sm_object *obj) { pw_log_debug("remove global '%u' %p monitor:%d", obj->id, obj, obj->monitor_global); pw_map_insert_at(&impl->globals, obj->id, NULL); sm_media_session_emit_remove(impl, obj); obj->id = SPA_ID_INVALID; } static void *find_object(struct impl *impl, uint32_t id, const char *type) { struct sm_object *obj; if ((obj = pw_map_lookup(&impl->globals, id)) == NULL) return NULL; if (type != NULL && !spa_streq(obj->type, type)) return NULL; return obj; } static struct data *object_find_data(struct sm_object *obj, const char *id) { struct data *d; spa_list_for_each(d, &obj->data, link) { if (spa_streq(d->id, id)) return d; } return NULL; } void *sm_object_add_data(struct sm_object *obj, const char *id, size_t size) { struct data *d; d = object_find_data(obj, id); if (d != NULL) { if (d->size == size) goto done; spa_list_remove(&d->link); free(d); } d = calloc(1, sizeof(struct data) + size); d->id = id; d->size = size; spa_list_append(&obj->data, &d->link); done: return SPA_PTROFF(d, sizeof(struct data), void); } void *sm_object_get_data(struct sm_object *obj, const char *id) { struct data *d; d = object_find_data(obj, id); if (d == NULL) return NULL; return SPA_PTROFF(d, sizeof(struct data), void); } int sm_object_remove_data(struct sm_object *obj, const char *id) { struct data *d; d = object_find_data(obj, id); if (d == NULL) return -ENOENT; spa_list_remove(&d->link); free(d); return 0; } static int sm_object_destroy_maybe_free(struct sm_object *obj) { struct impl *impl = SPA_CONTAINER_OF(obj->session, struct impl, this); struct data *d; pw_log_debug("%p: destroy object %p id:%d proxy:%p handle:%p monitor:%d destroyed:%d discarded:%d", obj->session, obj, obj->id, obj->proxy, obj->handle, obj->monitor_global, obj->destroyed, obj->discarded); if (obj->destroyed) goto unref; obj->destroyed = true; sm_object_emit_destroy(obj); if (SPA_FLAG_IS_SET(obj->mask, SM_OBJECT_CHANGE_MASK_LISTENER)) { SPA_FLAG_CLEAR(obj->mask, SM_OBJECT_CHANGE_MASK_LISTENER); spa_hook_remove(&obj->object_listener); } if (obj->id != SPA_ID_INVALID) remove_object(impl, obj); if (obj->destroy) obj->destroy(obj); spa_hook_remove(&obj->handle_listener); if (obj->proxy) { spa_hook_remove(&obj->proxy_listener); if (obj->proxy != obj->handle) pw_proxy_destroy(obj->proxy); obj->proxy = NULL; } pw_proxy_ref(obj->handle); pw_proxy_destroy(obj->handle); sm_object_emit_free(obj); unref: if (!obj->discarded) return 0; pw_properties_free(obj->props); obj->props = NULL; spa_list_consume(d, &obj->data, link) { spa_list_remove(&d->link); free(d); } spa_list_remove(&obj->link); pw_proxy_unref(obj->handle); /* frees obj */ return 0; } int sm_object_destroy(struct sm_object *obj) { sm_object_discard(obj); return sm_object_destroy_maybe_free(obj); } static struct param *add_param(struct spa_list *param_list, int seq, int *param_seq, uint32_t id, const struct spa_pod *param) { struct param *p; if (param == NULL || !spa_pod_is_object(param)) { errno = EINVAL; return NULL; } if (id == SPA_ID_INVALID) id = SPA_POD_OBJECT_ID(param); if (id >= SM_MAX_PARAMS) { pw_log_error("too big param id %d", id); errno = EINVAL; return NULL; } if (seq != param_seq[id]) { pw_log_debug("ignoring param %d, seq:%d != current_seq:%d", id, seq, param_seq[id]); errno = EBUSY; return NULL; } p = malloc(sizeof(struct param) + SPA_POD_SIZE(param)); if (p == NULL) return NULL; p->this.id = id; p->this.param = SPA_PTROFF(p, sizeof(struct param), struct spa_pod); memcpy(p->this.param, param, SPA_POD_SIZE(param)); spa_list_append(param_list, &p->this.link); return p; } static uint32_t clear_params(struct spa_list *param_list, uint32_t id) { struct param *p, *t; uint32_t count = 0; spa_list_for_each_safe(p, t, param_list, this.link) { if (id == SPA_ID_INVALID || p->this.id == id) { spa_list_remove(&p->this.link); free(p); count++; } } return count; } /** * Core */ static const struct object_info core_object_info = { .type = PW_TYPE_INTERFACE_Core, .version = PW_VERSION_CORE, .size = sizeof(struct sm_object), .init = NULL, }; /** * Module */ static const struct object_info module_info = { .type = PW_TYPE_INTERFACE_Module, .version = PW_VERSION_MODULE, .size = sizeof(struct sm_object), .init = NULL, }; /** * Factory */ static const struct object_info factory_info = { .type = PW_TYPE_INTERFACE_Factory, .version = PW_VERSION_FACTORY, .size = sizeof(struct sm_object), .init = NULL, }; /** * Clients */ static void client_event_info(void *object, const struct pw_client_info *info) { struct sm_client *client = object; struct impl *impl = SPA_CONTAINER_OF(client->obj.session, struct impl, this); pw_log_debug("%p: client %d info", impl, client->obj.id); client->info = pw_client_info_merge(client->info, info, client->obj.changed == 0); client->obj.avail |= SM_CLIENT_CHANGE_MASK_INFO; client->obj.changed |= SM_CLIENT_CHANGE_MASK_INFO; sm_object_sync_update(&client->obj); } static const struct pw_client_events client_events = { PW_VERSION_CLIENT_EVENTS, .info = client_event_info, }; static void client_destroy(void *object) { struct sm_client *client = object; if (client->info) pw_client_info_free(client->info); } static const struct object_info client_info = { .type = PW_TYPE_INTERFACE_Client, .version = PW_VERSION_CLIENT, .events = &client_events, .size = sizeof(struct sm_client), .init = NULL, .destroy = client_destroy, }; /** * Device */ static void device_event_info(void *object, const struct pw_device_info *info) { struct sm_device *device = object; struct impl *impl = SPA_CONTAINER_OF(device->obj.session, struct impl, this); uint32_t i; pw_log_debug("%p: device %d info", impl, device->obj.id); info = device->info = pw_device_info_merge(device->info, info, device->obj.changed == 0); device->obj.avail |= SM_DEVICE_CHANGE_MASK_INFO; device->obj.changed |= SM_DEVICE_CHANGE_MASK_INFO; if (info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) { for (i = 0; i < info->n_params; i++) { uint32_t id = info->params[i].id; if (info->params[i].user == 0) continue; if (id >= SM_MAX_PARAMS) { pw_log_error("%p: too big param id %d", impl, id); continue; } device->n_params -= clear_params(&device->param_list, id); if (info->params[i].flags & SPA_PARAM_INFO_READ) { int res; res = pw_device_enum_params((struct pw_device*)device->obj.proxy, ++device->param_seq[id], id, 0, UINT32_MAX, NULL); if (SPA_RESULT_IS_ASYNC(res)) device->param_seq[id] = res; pw_log_debug("%p: device %d enum params %d seq:%d", impl, device->obj.id, id, device->param_seq[id]); } info->params[i].user = 0; } } sm_object_sync_update(&device->obj); sm_media_session_schedule_rescan(&impl->this); } static void device_event_param(void *object, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param) { struct sm_device *device = object; struct impl *impl = SPA_CONTAINER_OF(device->obj.session, struct impl, this); pw_log_debug("%p: device %p param %d index:%d seq:%d", impl, device, id, index, seq); if (add_param(&device->param_list, seq, device->param_seq, id, param) != NULL) device->n_params++; device->obj.avail |= SM_DEVICE_CHANGE_MASK_PARAMS; device->obj.changed |= SM_DEVICE_CHANGE_MASK_PARAMS; } static const struct pw_device_events device_events = { PW_VERSION_DEVICE_EVENTS, .info = device_event_info, .param = device_event_param, }; static int device_init(void *object) { struct sm_device *device = object; spa_list_init(&device->node_list); spa_list_init(&device->param_list); return 0; } static void device_destroy(void *object) { struct sm_device *device = object; struct sm_node *node; spa_list_consume(node, &device->node_list, link) { node->device = NULL; spa_list_remove(&node->link); } clear_params(&device->param_list, SPA_ID_INVALID); device->n_params = 0; if (device->info) pw_device_info_free(device->info); device->info = NULL; } static const struct object_info device_info = { .type = PW_TYPE_INTERFACE_Device, .version = PW_VERSION_DEVICE, .events = &device_events, .size = sizeof(struct sm_device), .init = device_init, .destroy = device_destroy, }; static const struct object_info spa_device_info = { .type = SPA_TYPE_INTERFACE_Device, .version = SPA_VERSION_DEVICE, .size = sizeof(struct sm_device), .init = device_init, .destroy = device_destroy, }; /** * Node */ static void node_event_info(void *object, const struct pw_node_info *info) { struct sm_node *node = object; struct impl *impl = SPA_CONTAINER_OF(node->obj.session, struct impl, this); uint32_t i; pw_log_debug("%p: node %d info", impl, node->obj.id); info = node->info = pw_node_info_merge(node->info, info, node->obj.changed == 0); node->obj.avail |= SM_NODE_CHANGE_MASK_INFO; node->obj.changed |= SM_NODE_CHANGE_MASK_INFO; if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS && (node->obj.mask & SM_NODE_CHANGE_MASK_PARAMS)) { for (i = 0; i < info->n_params; i++) { uint32_t id = info->params[i].id; if (info->params[i].user == 0) continue; if (id >= SM_MAX_PARAMS) { pw_log_error("%p: too big param id %d", impl, id); continue; } node->n_params -= clear_params(&node->param_list, id); if (info->params[i].flags & SPA_PARAM_INFO_READ) { int res; res = pw_node_enum_params((struct pw_node*)node->obj.proxy, ++node->param_seq[id], id, 0, UINT32_MAX, NULL); if (SPA_RESULT_IS_ASYNC(res)) node->param_seq[id] = res; pw_log_debug("%p: node %d enum params %d seq:%d", impl, node->obj.id, id, node->param_seq[id]); } info->params[i].user = 0; } } sm_object_sync_update(&node->obj); sm_media_session_schedule_rescan(&impl->this); } static void node_event_param(void *object, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param) { struct sm_node *node = object; struct impl *impl = SPA_CONTAINER_OF(node->obj.session, struct impl, this); pw_log_debug("%p: node %p param %d index:%d seq:%d", impl, node, id, index, seq); if (add_param(&node->param_list, seq, node->param_seq, id, param) != NULL) node->n_params++; node->obj.avail |= SM_NODE_CHANGE_MASK_PARAMS; node->obj.changed |= SM_NODE_CHANGE_MASK_PARAMS; } static const struct pw_node_events node_events = { PW_VERSION_NODE_EVENTS, .info = node_event_info, .param = node_event_param, }; static int node_init(void *object) { struct sm_node *node = object; struct impl *impl = SPA_CONTAINER_OF(node->obj.session, struct impl, this); struct pw_properties *props = node->obj.props; spa_list_init(&node->port_list); spa_list_init(&node->param_list); if (props) { uint32_t id = SPA_ID_INVALID; if (pw_properties_fetch_uint32(props, PW_KEY_DEVICE_ID, &id) == 0) node->device = find_object(impl, id, NULL); pw_log_debug("%p: node %d parent device %d (%p)", impl, node->obj.id, id, node->device); if (node->device) { spa_list_append(&node->device->node_list, &node->link); node->device->obj.avail |= SM_DEVICE_CHANGE_MASK_NODES; node->device->obj.changed |= SM_DEVICE_CHANGE_MASK_NODES; } } return 0; } static void node_destroy(void *object) { struct sm_node *node = object; struct sm_port *port; spa_list_consume(port, &node->port_list, link) { port->node = NULL; spa_list_remove(&port->link); } clear_params(&node->param_list, SPA_ID_INVALID); node->n_params = 0; if (node->device) { spa_list_remove(&node->link); node->device->obj.changed |= SM_DEVICE_CHANGE_MASK_NODES; } if (node->info) { pw_node_info_free(node->info); node->info = NULL; } free(node->target_node); node->target_node = NULL; } static const struct object_info node_info = { .type = PW_TYPE_INTERFACE_Node, .version = PW_VERSION_NODE, .events = &node_events, .size = sizeof(struct sm_node), .init = node_init, .destroy = node_destroy, }; /** * Port */ static void port_event_info(void *object, const struct pw_port_info *info) { struct sm_port *port = object; struct impl *impl = SPA_CONTAINER_OF(port->obj.session, struct impl, this); pw_log_debug("%p: port %d info", impl, port->obj.id); port->info = pw_port_info_merge(port->info, info, port->obj.changed == 0); port->obj.avail |= SM_PORT_CHANGE_MASK_INFO; port->obj.changed |= SM_PORT_CHANGE_MASK_INFO; sm_object_sync_update(&port->obj); } static const struct pw_port_events port_events = { PW_VERSION_PORT_EVENTS, .info = port_event_info, }; static enum spa_audio_channel find_channel(const char *name) { int i; for (i = 0; spa_type_audio_channel[i].name; i++) { if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) return spa_type_audio_channel[i].type; } return SPA_AUDIO_CHANNEL_UNKNOWN; } static int port_init(void *object) { struct sm_port *port = object; struct impl *impl = SPA_CONTAINER_OF(port->obj.session, struct impl, this); struct pw_properties *props = port->obj.props; const char *str; if (props) { uint32_t id = SPA_ID_INVALID; if ((str = pw_properties_get(props, PW_KEY_PORT_DIRECTION)) != NULL) port->direction = spa_streq(str, "out") ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT; if ((str = pw_properties_get(props, PW_KEY_FORMAT_DSP)) != NULL) { if (spa_streq(str, "32 bit float mono audio")) port->type = SM_PORT_TYPE_DSP_AUDIO; else if (spa_streq(str, "8 bit raw midi")) port->type = SM_PORT_TYPE_DSP_MIDI; } if ((str = pw_properties_get(props, PW_KEY_AUDIO_CHANNEL)) != NULL) port->channel = find_channel(str); if (pw_properties_fetch_uint32(props, PW_KEY_NODE_ID, &id) == 0) port->node = find_object(impl, id, PW_TYPE_INTERFACE_Node); pw_log_debug("%p: port %d parent node %s (%p) direction:%d type:%d", impl, port->obj.id, str, port->node, port->direction, port->type); if (port->node) { spa_list_append(&port->node->port_list, &port->link); port->node->obj.avail |= SM_NODE_CHANGE_MASK_PORTS; port->node->obj.changed |= SM_NODE_CHANGE_MASK_PORTS; } } return 0; } static void port_destroy(void *object) { struct sm_port *port = object; if (port->info) pw_port_info_free(port->info); if (port->node) { spa_list_remove(&port->link); port->node->obj.changed |= SM_NODE_CHANGE_MASK_PORTS; } } static const struct object_info port_info = { .type = PW_TYPE_INTERFACE_Port, .version = PW_VERSION_PORT, .events = &port_events, .size = sizeof(struct sm_port), .init = port_init, .destroy = port_destroy, }; /** * Session */ static void session_event_info(void *object, const struct pw_session_info *info) { struct sm_session *sess = object; struct impl *impl = SPA_CONTAINER_OF(sess->obj.session, struct impl, this); struct pw_session_info *i = sess->info; pw_log_debug("%p: session %d info", impl, sess->obj.id); if (i == NULL && info) { i = sess->info = calloc(1, sizeof(struct pw_session_info)); i->version = PW_VERSION_SESSION_INFO; i->id = info->id; } if (info) { i->change_mask = info->change_mask; if (info->change_mask & PW_SESSION_CHANGE_MASK_PROPS) { pw_properties_free ((struct pw_properties *)i->props); i->props = (struct spa_dict *) pw_properties_new_dict (info->props); } } sess->obj.avail |= SM_SESSION_CHANGE_MASK_INFO; sess->obj.changed |= SM_SESSION_CHANGE_MASK_INFO; sm_object_sync_update(&sess->obj); } static const struct pw_session_events session_events = { PW_VERSION_SESSION_EVENTS, .info = session_event_info, }; static int session_init(void *object) { struct sm_session *sess = object; struct impl *impl = SPA_CONTAINER_OF(sess->obj.session, struct impl, this); if (sess->obj.id == impl->this.session_id) impl->this.session = sess; spa_list_init(&sess->endpoint_list); return 0; } static void session_destroy(void *object) { struct sm_session *sess = object; struct sm_endpoint *endpoint; struct pw_session_info *i = sess->info; spa_list_consume(endpoint, &sess->endpoint_list, link) { endpoint->session = NULL; spa_list_remove(&endpoint->link); } if (i) { pw_properties_free ((struct pw_properties *)i->props); free(i); } } static const struct object_info session_info = { .type = PW_TYPE_INTERFACE_Session, .version = PW_VERSION_SESSION, .events = &session_events, .size = sizeof(struct sm_session), .init = session_init, .destroy = session_destroy, }; /** * Endpoint */ static void endpoint_event_info(void *object, const struct pw_endpoint_info *info) { struct sm_endpoint *endpoint = object; struct impl *impl = SPA_CONTAINER_OF(endpoint->obj.session, struct impl, this); struct pw_endpoint_info *i = endpoint->info; const char *str; pw_log_debug("%p: endpoint %d info", impl, endpoint->obj.id); if (i == NULL && info) { i = endpoint->info = calloc(1, sizeof(struct pw_endpoint_info)); i->id = info->id; i->name = info->name ? strdup(info->name) : NULL; i->media_class = info->media_class ? strdup(info->media_class) : NULL; i->direction = info->direction; i->flags = info->flags; } if (info) { i->change_mask = info->change_mask; if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_SESSION) { i->session_id = info->session_id; } if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS) { pw_properties_free ((struct pw_properties *)i->props); i->props = (struct spa_dict *) pw_properties_new_dict (info->props); if ((str = spa_dict_lookup(i->props, PW_KEY_PRIORITY_SESSION)) != NULL) endpoint->priority = pw_properties_parse_int(str); } } endpoint->obj.avail |= SM_ENDPOINT_CHANGE_MASK_INFO; endpoint->obj.changed |= SM_ENDPOINT_CHANGE_MASK_INFO; sm_object_sync_update(&endpoint->obj); } static const struct pw_endpoint_events endpoint_events = { PW_VERSION_ENDPOINT_EVENTS, .info = endpoint_event_info, }; static int endpoint_init(void *object) { struct sm_endpoint *endpoint = object; struct impl *impl = SPA_CONTAINER_OF(endpoint->obj.session, struct impl, this); struct pw_properties *props = endpoint->obj.props; if (props) { uint32_t id = SPA_ID_INVALID; if (pw_properties_fetch_uint32(props, PW_KEY_SESSION_ID, &id) == 0) endpoint->session = find_object(impl, id, PW_TYPE_INTERFACE_Session); pw_log_debug("%p: endpoint %d parent session %d", impl, endpoint->obj.id, id); if (endpoint->session) { spa_list_append(&endpoint->session->endpoint_list, &endpoint->link); endpoint->session->obj.avail |= SM_SESSION_CHANGE_MASK_ENDPOINTS; endpoint->session->obj.changed |= SM_SESSION_CHANGE_MASK_ENDPOINTS; } } spa_list_init(&endpoint->stream_list); return 0; } static void endpoint_destroy(void *object) { struct sm_endpoint *endpoint = object; struct sm_endpoint_stream *stream; struct pw_endpoint_info *i = endpoint->info; spa_list_consume(stream, &endpoint->stream_list, link) { stream->endpoint = NULL; spa_list_remove(&stream->link); } if (endpoint->session) { endpoint->session = NULL; spa_list_remove(&endpoint->link); } if (i) { pw_properties_free ((struct pw_properties *)i->props); free(i->name); free(i->media_class); free(i); } } static const struct object_info endpoint_info = { .type = PW_TYPE_INTERFACE_Endpoint, .version = PW_VERSION_ENDPOINT, .events = &endpoint_events, .size = sizeof(struct sm_endpoint), .init = endpoint_init, .destroy = endpoint_destroy, }; /** * Endpoint Stream */ static void endpoint_stream_event_info(void *object, const struct pw_endpoint_stream_info *info) { struct sm_endpoint_stream *stream = object; struct impl *impl = SPA_CONTAINER_OF(stream->obj.session, struct impl, this); pw_log_debug("%p: endpoint stream %d info", impl, stream->obj.id); if (stream->info == NULL && info) { stream->info = calloc(1, sizeof(struct pw_endpoint_stream_info)); stream->info->version = PW_VERSION_ENDPOINT_STREAM_INFO; stream->info->id = info->id; stream->info->endpoint_id = info->endpoint_id; stream->info->name = info->name ? strdup(info->name) : NULL; } if (info) { stream->info->change_mask = info->change_mask; } stream->obj.avail |= SM_ENDPOINT_CHANGE_MASK_INFO; stream->obj.changed |= SM_ENDPOINT_CHANGE_MASK_INFO; sm_object_sync_update(&stream->obj); } static const struct pw_endpoint_stream_events endpoint_stream_events = { PW_VERSION_ENDPOINT_STREAM_EVENTS, .info = endpoint_stream_event_info, }; static int endpoint_stream_init(void *object) { struct sm_endpoint_stream *stream = object; struct impl *impl = SPA_CONTAINER_OF(stream->obj.session, struct impl, this); struct pw_properties *props = stream->obj.props; if (props) { uint32_t id = SPA_ID_INVALID; if (pw_properties_fetch_uint32(props, PW_KEY_ENDPOINT_ID, &id) == 0) stream->endpoint = find_object(impl, id, PW_TYPE_INTERFACE_Endpoint); pw_log_debug("%p: stream %d parent endpoint %d", impl, stream->obj.id, id); if (stream->endpoint) { spa_list_append(&stream->endpoint->stream_list, &stream->link); stream->endpoint->obj.avail |= SM_ENDPOINT_CHANGE_MASK_STREAMS; stream->endpoint->obj.changed |= SM_ENDPOINT_CHANGE_MASK_STREAMS; } } spa_list_init(&stream->link_list); return 0; } static void endpoint_stream_destroy(void *object) { struct sm_endpoint_stream *stream = object; if (stream->info) { free(stream->info->name); free(stream->info); } if (stream->endpoint) { stream->endpoint = NULL; spa_list_remove(&stream->link); } } static const struct object_info endpoint_stream_info = { .type = PW_TYPE_INTERFACE_EndpointStream, .version = PW_VERSION_ENDPOINT_STREAM, .events = &endpoint_stream_events, .size = sizeof(struct sm_endpoint_stream), .init = endpoint_stream_init, .destroy = endpoint_stream_destroy, }; /** * Endpoint Link */ static void endpoint_link_event_info(void *object, const struct pw_endpoint_link_info *info) { struct sm_endpoint_link *link = object; struct impl *impl = SPA_CONTAINER_OF(link->obj.session, struct impl, this); pw_log_debug("%p: endpoint link %d info", impl, link->obj.id); if (link->info == NULL && info) { link->info = calloc(1, sizeof(struct pw_endpoint_link_info)); link->info->version = PW_VERSION_ENDPOINT_LINK_INFO; link->info->id = info->id; link->info->session_id = info->session_id; link->info->output_endpoint_id = info->output_endpoint_id; link->info->output_stream_id = info->output_stream_id; link->info->input_endpoint_id = info->input_endpoint_id; link->info->input_stream_id = info->input_stream_id; } if (info) { link->info->change_mask = info->change_mask; } link->obj.avail |= SM_ENDPOINT_LINK_CHANGE_MASK_INFO; link->obj.changed |= SM_ENDPOINT_LINK_CHANGE_MASK_INFO; sm_object_sync_update(&link->obj); } static const struct pw_endpoint_link_events endpoint_link_events = { PW_VERSION_ENDPOINT_LINK_EVENTS, .info = endpoint_link_event_info, }; static void endpoint_link_destroy(void *object) { struct sm_endpoint_link *link = object; if (link->info) { free(link->info->error); free(link->info); } if (link->output) { link->output = NULL; spa_list_remove(&link->output_link); } if (link->input) { link->input = NULL; spa_list_remove(&link->input_link); } } static const struct object_info endpoint_link_info = { .type = PW_TYPE_INTERFACE_EndpointLink, .version = PW_VERSION_ENDPOINT_LINK, .events = &endpoint_link_events, .size = sizeof(struct sm_endpoint_link), .init = NULL, .destroy = endpoint_link_destroy, }; /** * Proxy */ static void done_proxy(void *data, int seq) { struct sm_object *obj = data; pw_log_debug("done %p proxy %p avail:%08x update:%08x %d/%d", obj, obj->proxy, obj->avail, obj->changed, obj->pending, seq); if (obj->pending == seq) { obj->pending = SPA_ID_INVALID; if (obj->changed) sm_object_emit_update(obj); obj->changed = 0; } } static const struct pw_proxy_events proxy_events = { PW_VERSION_PROXY_EVENTS, .done = done_proxy, }; static void bound_handle(void *data, uint32_t id) { struct sm_object *obj = data; struct impl *impl = SPA_CONTAINER_OF(obj->session, struct impl, this); pw_log_debug("bound %p proxy %p handle %p id:%d->%d", obj, obj->proxy, obj->handle, obj->id, id); if (obj->id == SPA_ID_INVALID) { struct sm_object *old_obj = find_object(impl, id, NULL); if (old_obj != NULL) { /* * Monitor core is always more up-to-date in object creation * events (see registry_global), so in case of duplicate objects * we should prefer monitor globals. */ if (obj->monitor_global) sm_object_destroy_maybe_free(old_obj); else { sm_object_destroy_maybe_free(obj); return; } } add_object(impl, obj, id); } } static const struct pw_proxy_events handle_events = { PW_VERSION_PROXY_EVENTS, .bound = bound_handle, }; int sm_object_sync_update(struct sm_object *obj) { obj->pending = pw_proxy_sync(obj->proxy, 1); pw_log_debug("sync %p proxy %p %d", obj, obj->proxy, obj->pending); return obj->pending; } static const struct object_info *get_object_info(struct impl *impl, const char *type) { const struct object_info *info; if (spa_streq(type, PW_TYPE_INTERFACE_Core)) info = &core_object_info; else if (spa_streq(type, PW_TYPE_INTERFACE_Module)) info = &module_info; else if (spa_streq(type, PW_TYPE_INTERFACE_Factory)) info = &factory_info; else if (spa_streq(type, PW_TYPE_INTERFACE_Client)) info = &client_info; else if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) info = &spa_device_info; else if (spa_streq(type, PW_TYPE_INTERFACE_Device)) info = &device_info; else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) info = &node_info; else if (spa_streq(type, PW_TYPE_INTERFACE_Port)) info = &port_info; else if (spa_streq(type, PW_TYPE_INTERFACE_Session)) info = &session_info; else if (spa_streq(type, PW_TYPE_INTERFACE_Endpoint)) info = &endpoint_info; else if (spa_streq(type, PW_TYPE_INTERFACE_EndpointStream)) info = &endpoint_stream_info; else if (spa_streq(type, PW_TYPE_INTERFACE_EndpointLink)) info = &endpoint_link_info; else info = NULL; return info; } static struct sm_object *init_object(struct impl *impl, const struct object_info *info, struct pw_proxy *proxy, struct pw_proxy *handle, uint32_t id, const struct spa_dict *props, bool monitor_global) { struct sm_object *obj; obj = pw_proxy_get_user_data(handle); obj->session = &impl->this; obj->id = id; obj->type = info->type; obj->props = props ? pw_properties_new_dict(props) : pw_properties_new(NULL, NULL); obj->proxy = proxy; obj->handle = handle; obj->destroy = info->destroy; obj->mask |= SM_OBJECT_CHANGE_MASK_PROPERTIES | SM_OBJECT_CHANGE_MASK_BIND; obj->avail |= obj->mask; obj->monitor_global = monitor_global; spa_hook_list_init(&obj->hooks); spa_list_init(&obj->data); spa_list_append(&impl->object_list, &obj->link); if (proxy) { pw_proxy_add_listener(obj->proxy, &obj->proxy_listener, &proxy_events, obj); if (info->events != NULL) pw_proxy_add_object_listener(obj->proxy, &obj->object_listener, info->events, obj); SPA_FLAG_UPDATE(obj->mask, SM_OBJECT_CHANGE_MASK_LISTENER, info->events != NULL); } pw_proxy_add_listener(obj->handle, &obj->handle_listener, &handle_events, obj); if (info->init) info->init(obj); return obj; } static struct sm_object * create_object(struct impl *impl, struct pw_proxy *proxy, struct pw_proxy *handle, const struct spa_dict *props, bool monitor_global) { const char *type; const struct object_info *info; struct sm_object *obj; type = pw_proxy_get_type(handle, NULL); if (spa_streq(type, PW_TYPE_INTERFACE_ClientNode)) type = PW_TYPE_INTERFACE_Node; info = get_object_info(impl, type); if (info == NULL) { pw_log_error("%p: unknown object type %s", impl, type); errno = ENOTSUP; return NULL; } obj = init_object(impl, info, proxy, handle, SPA_ID_INVALID, props, monitor_global); pw_log_debug("%p: created new object %p proxy:%p handle:%p", impl, obj, obj->proxy, obj->handle); return obj; } static struct sm_object * bind_object(struct impl *impl, const struct object_info *info, struct registry_event *re) { struct pw_proxy *proxy; struct sm_object *obj; proxy = re->proxy; re->proxy = NULL; obj = init_object(impl, info, proxy, proxy, re->id, re->props, false); sm_object_discard(obj); add_object(impl, obj, re->id); pw_log_debug("%p: bound new object %p proxy %p id:%d", impl, obj, obj->proxy, obj->id); return obj; } static int update_object(struct impl *impl, const struct object_info *info, struct sm_object *obj, struct registry_event *re) { struct pw_proxy *proxy; pw_properties_update(obj->props, re->props); if (obj->proxy != NULL) return 0; pw_log_debug("%p: update type:%s", impl, obj->type); proxy = re->proxy; re->proxy = NULL; obj->proxy = proxy; obj->type = info->type; pw_proxy_add_listener(obj->proxy, &obj->proxy_listener, &proxy_events, obj); if (info->events) pw_proxy_add_object_listener(obj->proxy, &obj->object_listener, info->events, obj); SPA_FLAG_UPDATE(obj->mask, SM_OBJECT_CHANGE_MASK_LISTENER, info->events != NULL); sm_media_session_emit_create(impl, obj); return 0; } static void registry_event_free(struct registry_event *re) { if (re->proxy) pw_proxy_destroy(re->proxy); pw_properties_free(re->props_store); if (re->allocated) { spa_list_remove(&re->link); free(re); } else { spa_zero(*re); } } static int handle_registry_event(struct impl *impl, struct registry_event *re) { struct sm_object *obj; const struct object_info *info = NULL; obj = find_object(impl, re->id, NULL); pw_log_debug("%p: new global '%d' %s/%d obj:%p monitor:%d seq:%d", impl, re->id, re->type, re->version, obj, re->monitor, re->seq); info = get_object_info(impl, re->type); if (info == NULL) return 0; if (obj == NULL && !re->monitor) { /* * Only policy core binds new objects. * * The monitor core event corresponding to this one has already been * processed. If monitor doesn't have the id now, the object either has * not been created there, or there is a race condition and it was already * removed. In that case, we create a zombie object here, but its remove * event is already queued and arrives soon. */ bind_object(impl, info, re); } else if (obj != NULL && obj->monitor_global == re->monitor) { /* Each core handles their own object updates */ update_object(impl, info, obj, re); } sm_media_session_schedule_rescan(&impl->this); return 0; } static int handle_postponed_registry_events(struct impl *impl, int seq) { struct registry_event *re, *t; spa_list_for_each_safe(re, t, &impl->registry_event_list, link) { if (re->seq == seq) { handle_registry_event(impl, re); registry_event_free(re); } } return 0; } static int monitor_sync(struct impl *impl) { pw_core_set_paused(impl->policy_core, true); impl->monitor_seq = pw_core_sync(impl->monitor_core, 0, impl->monitor_seq); pw_log_debug("%p: monitor sync start %d", impl, impl->monitor_seq); sm_media_session_schedule_rescan(&impl->this); return impl->monitor_seq; } static void registry_global(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { struct impl *impl = data; const struct object_info *info; struct registry_event *re = NULL; static bool warned_about_wireplumber = false; info = get_object_info(impl, type); if (info == NULL) return; pw_log_debug("%p: registry event (policy) for new global '%d'", impl, id); if (!warned_about_wireplumber && props && spa_streq(info->type, PW_TYPE_INTERFACE_Client)) { const char *name = spa_dict_lookup(props, PW_KEY_APP_NAME); if (spa_streq(name, "WirePlumber")) { pw_log_error("WirePlumber appears to be running; " "please stop it before starting pipewire-media-session"); warned_about_wireplumber = true; } } /* * Handle policy core events after monitor core ones. * * Monitor sync pauses policy core, so the event will be handled before * further registry or proxy events are received via policy core. */ re = calloc(1, sizeof(struct registry_event)); if (re == NULL) goto error; re->allocated = true; spa_list_append(&impl->registry_event_list, &re->link); re->id = id; re->monitor = false; re->permissions = permissions; re->type = info->type; re->version = version; /* Bind proxy now */ re->proxy = pw_registry_bind(impl->registry, id, type, info->version, info->size); if (re->proxy == NULL) goto error; if (props) { re->props_store = pw_properties_new_dict(props); if (re->props_store == NULL) goto error; re->props = &re->props_store->dict; } re->seq = monitor_sync(impl); return; error: if (re) registry_event_free(re); pw_log_warn("%p: can't handle global %d: %s", impl, id, spa_strerror(-errno)); } static void registry_global_remove(void *data, uint32_t id) { struct impl *impl = data; struct sm_object *obj; obj = find_object(impl, id, NULL); obj = (obj && !obj->monitor_global) ? obj : NULL; pw_log_debug("%p: registry event (policy) for remove global '%d' obj:%p", impl, id, obj); if (obj) sm_object_destroy_maybe_free(obj); } static const struct pw_registry_events registry_events = { PW_VERSION_REGISTRY_EVENTS, .global = registry_global, .global_remove = registry_global_remove, }; static void monitor_registry_global(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { struct impl *impl = data; const struct object_info *info; struct registry_event re = { .id = id, .permissions = permissions, .type = type, .version = version, .props = props, .monitor = true }; pw_log_debug("%p: registry event (monitor) for new global '%d'", impl, id); info = get_object_info(impl, type); if (info == NULL) return; /* Bind proxy now from policy core */ re.proxy = pw_registry_bind(impl->registry, id, type, info->version, 0); if (re.proxy) handle_registry_event(impl, &re); else pw_log_warn("%p: can't handle global %d: %s", impl, id, spa_strerror(-errno)); registry_event_free(&re); return; } static void monitor_registry_global_remove(void *data, uint32_t id) { struct impl *impl = data; struct sm_object *obj; obj = find_object(impl, id, NULL); obj = (obj && obj->monitor_global) ? obj : NULL; pw_log_debug("%p: registry event (monitor) for remove global '%d' obj:%p", impl, id, obj); if (obj) sm_object_destroy_maybe_free(obj); } static const struct pw_registry_events monitor_registry_events = { PW_VERSION_REGISTRY_EVENTS, .global = monitor_registry_global, .global_remove = monitor_registry_global_remove, }; int sm_object_add_listener(struct sm_object *obj, struct spa_hook *listener, const struct sm_object_events *events, void *data) { spa_hook_list_append(&obj->hooks, listener, events, data); return 0; } int sm_media_session_add_listener(struct sm_media_session *sess, struct spa_hook *listener, const struct sm_media_session_events *events, void *data) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); struct spa_hook_list save; struct sm_object *obj; spa_hook_list_isolate(&impl->hooks, &save, listener, events, data); spa_list_for_each(obj, &impl->object_list, link) { if (obj->id == SPA_ID_INVALID) continue; sm_media_session_emit_create(impl, obj); } spa_hook_list_join(&impl->hooks, &save); return 0; } struct sm_object *sm_media_session_find_object(struct sm_media_session *sess, uint32_t id) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); return find_object(impl, id, NULL); } int sm_media_session_destroy_object(struct sm_media_session *sess, uint32_t id) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); pw_registry_destroy(impl->registry, id); return 0; } int sm_media_session_for_each_object(struct sm_media_session *sess, int (*callback) (void *data, struct sm_object *object), void *data) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); struct sm_object *obj; int res; spa_list_for_each(obj, &impl->object_list, link) { if (obj->id == SPA_ID_INVALID) continue; if ((res = callback(data, obj)) != 0) return res; } return 0; } int sm_media_session_schedule_rescan(struct sm_media_session *sess) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); if (impl->scanning) { impl->rescan_pending = true; return impl->rescan_seq; } if (impl->policy_core) impl->rescan_seq = pw_core_sync(impl->policy_core, 0, impl->last_seq); return impl->rescan_seq; } int sm_media_session_sync(struct sm_media_session *sess, void (*callback) (void *data), void *data) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); struct sync *sync; sync = calloc(1, sizeof(struct sync)); if (sync == NULL) return -errno; spa_list_append(&impl->sync_list, &sync->link); sync->callback = callback; sync->data = data; sync->seq = pw_core_sync(impl->policy_core, 0, impl->last_seq); return sync->seq; } static void roundtrip_callback(void *data) { int *done = data; *done = 1; } int sm_media_session_roundtrip(struct sm_media_session *sess) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); struct pw_loop *loop = impl->this.loop; int done, res, seq; if (impl->policy_core == NULL) return -EIO; done = 0; if ((seq = sm_media_session_sync(sess, roundtrip_callback, &done)) < 0) return seq; pw_log_debug("%p: roundtrip %d", impl, seq); pw_loop_enter(loop); while (!done) { if ((res = pw_loop_iterate(loop, -1)) < 0) { if (res == -EINTR) continue; pw_log_warn("%p: iterate error %d (%s)", loop, res, spa_strerror(res)); break; } } pw_loop_leave(loop); pw_log_debug("%p: roundtrip %d done", impl, seq); return 0; } struct pw_proxy *sm_media_session_export(struct sm_media_session *sess, const char *type, const struct spa_dict *props, void *object, size_t user_data_size) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); struct pw_proxy *handle; pw_log_debug("%p: object %s %p", impl, type, object); handle = pw_core_export(impl->monitor_core, type, props, object, user_data_size); monitor_sync(impl); return handle; } struct sm_node *sm_media_session_export_node(struct sm_media_session *sess, const struct spa_dict *props, struct pw_impl_node *object) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); struct sm_node *node; struct pw_proxy *handle; pw_log_debug("%p: node %p", impl, object); handle = pw_core_export(impl->monitor_core, PW_TYPE_INTERFACE_Node, props, object, sizeof(struct sm_node)); node = (struct sm_node *) create_object(impl, NULL, handle, props, true); monitor_sync(impl); return node; } struct sm_device *sm_media_session_export_device(struct sm_media_session *sess, const struct spa_dict *props, struct spa_device *object) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); struct sm_device *device; struct pw_proxy *handle; pw_log_debug("%p: device %p", impl, object); handle = pw_core_export(impl->monitor_core, SPA_TYPE_INTERFACE_Device, props, object, sizeof(struct sm_device)); device = (struct sm_device *) create_object(impl, NULL, handle, props, true); monitor_sync(impl); return device; } struct pw_proxy *sm_media_session_create_object(struct sm_media_session *sess, const char *factory_name, const char *type, uint32_t version, const struct spa_dict *props, size_t user_data_size) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); return pw_core_create_object(impl->policy_core, factory_name, type, version, props, user_data_size); } struct sm_node *sm_media_session_create_node(struct sm_media_session *sess, const char *factory_name, const struct spa_dict *props) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); struct sm_node *node; struct pw_proxy *proxy; pw_log_debug("%p: node '%s'", impl, factory_name); proxy = pw_core_create_object(impl->policy_core, factory_name, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, props, sizeof(struct sm_node)); node = (struct sm_node *)create_object(impl, proxy, proxy, props, false); return node; } static void check_endpoint_link(struct endpoint_link *link) { if (!spa_list_is_empty(&link->link_list)) return; if (link->impl) { spa_list_remove(&link->link); pw_map_remove(&link->impl->endpoint_links, link->id); pw_client_session_link_update(link->impl->this.client_session, link->id, PW_CLIENT_SESSION_LINK_UPDATE_DESTROYED, 0, NULL, NULL); link->impl = NULL; free(link); } } static void proxy_link_error(void *data, int seq, int res, const char *message) { struct link *l = data; pw_log_info("can't link %d:%d -> %d:%d: %s", l->output_node, l->output_port, l->input_node, l->input_port, message); pw_proxy_destroy(l->proxy); } static void proxy_link_removed(void *data) { struct link *l = data; pw_proxy_destroy(l->proxy); } static void proxy_link_destroy(void *data) { struct link *l = data; spa_list_remove(&l->link); spa_hook_remove(&l->listener); if (l->endpoint_link) { check_endpoint_link(l->endpoint_link); l->endpoint_link = NULL; } } static const struct pw_proxy_events proxy_link_events = { PW_VERSION_PROXY_EVENTS, .error = proxy_link_error, .removed = proxy_link_removed, .destroy = proxy_link_destroy }; static bool channel_is_aux(uint32_t channel) { return channel >= SPA_AUDIO_CHANNEL_START_Aux && channel <= SPA_AUDIO_CHANNEL_LAST_Aux; } static int score_ports(struct sm_port *out, struct sm_port *in) { int score = 0; if (in->direction != PW_DIRECTION_INPUT || out->direction != PW_DIRECTION_OUTPUT) return 0; if (out->type != SM_PORT_TYPE_UNKNOWN && in->type != SM_PORT_TYPE_UNKNOWN && in->type != out->type) return 0; if (out->channel == in->channel) score += 100; else if ((out->channel == SPA_AUDIO_CHANNEL_SL && in->channel == SPA_AUDIO_CHANNEL_RL) || (out->channel == SPA_AUDIO_CHANNEL_RL && in->channel == SPA_AUDIO_CHANNEL_SL) || (out->channel == SPA_AUDIO_CHANNEL_SR && in->channel == SPA_AUDIO_CHANNEL_RR) || (out->channel == SPA_AUDIO_CHANNEL_RR && in->channel == SPA_AUDIO_CHANNEL_SR)) score += 60; else if ((out->channel == SPA_AUDIO_CHANNEL_FC && in->channel == SPA_AUDIO_CHANNEL_MONO) || (out->channel == SPA_AUDIO_CHANNEL_MONO && in->channel == SPA_AUDIO_CHANNEL_FC)) score += 50; else if (in->channel == SPA_AUDIO_CHANNEL_UNKNOWN || in->channel == SPA_AUDIO_CHANNEL_MONO || out->channel == SPA_AUDIO_CHANNEL_UNKNOWN || out->channel == SPA_AUDIO_CHANNEL_MONO) score += 10; else if (channel_is_aux(in->channel) != channel_is_aux(out->channel)) score += 7; if (score > 0 && !in->visited) score += 5; if (score <= 10) score = 0; return score; } static struct sm_port *find_input_port(struct impl *impl, struct sm_node *outnode, struct sm_port *outport, struct sm_node *innode) { struct sm_port *inport, *best_port = NULL; int score, best_score = 0; spa_list_for_each(inport, &innode->port_list, link) { score = score_ports(outport, inport); if (score > best_score) { best_score = score; best_port = inport; } } return best_port; } static int link_nodes(struct impl *impl, struct endpoint_link *link, struct sm_node *outnode, struct sm_node *innode) { struct pw_properties *props; struct sm_port *outport, *inport; int count = 0; bool passive = false; const char *str; pw_log_debug("%p: linking %d -> %d", impl, outnode->obj.id, innode->obj.id); if ((str = spa_dict_lookup(outnode->info->props, PW_KEY_NODE_PASSIVE)) != NULL) passive |= (pw_properties_parse_bool(str) || spa_streq(str, "out")); if ((str = spa_dict_lookup(innode->info->props, PW_KEY_NODE_PASSIVE)) != NULL) passive |= (pw_properties_parse_bool(str) || spa_streq(str, "in")); props = pw_properties_new(NULL, NULL); pw_properties_setf(props, PW_KEY_LINK_OUTPUT_NODE, "%d", outnode->obj.id); pw_properties_setf(props, PW_KEY_LINK_INPUT_NODE, "%d", innode->obj.id); pw_properties_setf(props, PW_KEY_LINK_PASSIVE, "%s", passive ? "true" : "false"); spa_list_for_each(inport, &innode->port_list, link) inport->visited = false; spa_list_for_each(outport, &outnode->port_list, link) { struct link *l; struct pw_proxy *p; if (outport->direction != PW_DIRECTION_OUTPUT) continue; inport = find_input_port(impl, outnode, outport, innode); if (inport == NULL) { pw_log_debug("%p: port %d:%d can't be linked", impl, outport->direction, outport->obj.id); continue; } inport->visited = true; pw_log_debug("%p: port %d:%d -> %d:%d", impl, outport->direction, outport->obj.id, inport->direction, inport->obj.id); pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%d", outport->obj.id); pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%d", inport->obj.id); p = pw_core_create_object(impl->policy_core, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, sizeof(struct link)); if (p == NULL) return -errno; l = pw_proxy_get_user_data(p); l->proxy = p; l->output_node = outnode->obj.id; l->output_port = outport->obj.id; l->input_node = innode->obj.id; l->input_port = inport->obj.id; pw_proxy_add_listener(p, &l->listener, &proxy_link_events, l); count++; if (link) { l->endpoint_link = link; spa_list_append(&link->link_list, &l->link); } else { spa_list_append(&impl->link_list, &l->link); } } pw_properties_free(props); return count; } int sm_media_session_create_links(struct sm_media_session *sess, const struct spa_dict *dict) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); struct sm_object *obj; struct sm_node *outnode = NULL, *innode = NULL; struct sm_endpoint *outendpoint = NULL, *inendpoint = NULL; struct sm_endpoint_stream *outstream = NULL, *instream = NULL; struct endpoint_link *link = NULL; const char *str; int res; sm_media_session_roundtrip(sess); /* find output node */ if ((str = spa_dict_lookup(dict, PW_KEY_LINK_OUTPUT_NODE)) != NULL && (obj = find_object(impl, atoi(str), PW_TYPE_INTERFACE_Node)) != NULL) outnode = (struct sm_node*)obj; /* find input node */ if ((str = spa_dict_lookup(dict, PW_KEY_LINK_INPUT_NODE)) != NULL && (obj = find_object(impl, atoi(str), PW_TYPE_INTERFACE_Node)) != NULL) innode = (struct sm_node*)obj; /* find endpoints and streams */ if ((str = spa_dict_lookup(dict, PW_KEY_ENDPOINT_LINK_OUTPUT_ENDPOINT)) != NULL && (obj = find_object(impl, atoi(str), PW_TYPE_INTERFACE_Endpoint)) != NULL) outendpoint = (struct sm_endpoint*)obj; if ((str = spa_dict_lookup(dict, PW_KEY_ENDPOINT_LINK_OUTPUT_STREAM)) != NULL && (obj = find_object(impl, atoi(str), PW_TYPE_INTERFACE_EndpointStream)) != NULL) outstream = (struct sm_endpoint_stream*)obj; if ((str = spa_dict_lookup(dict, PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT)) != NULL && (obj = find_object(impl, atoi(str), PW_TYPE_INTERFACE_Endpoint)) != NULL) inendpoint = (struct sm_endpoint*)obj; if ((str = spa_dict_lookup(dict, PW_KEY_ENDPOINT_LINK_INPUT_STREAM)) != NULL && (obj = find_object(impl, atoi(str), PW_TYPE_INTERFACE_EndpointStream)) != NULL) instream = (struct sm_endpoint_stream*)obj; if (outendpoint != NULL && inendpoint != NULL) { link = calloc(1, sizeof(struct endpoint_link)); if (link == NULL) return -errno; link->id = pw_map_insert_new(&impl->endpoint_links, link); link->impl = impl; spa_list_init(&link->link_list); spa_list_append(&impl->endpoint_link_list, &link->link); link->info.version = PW_VERSION_ENDPOINT_LINK_INFO; link->info.id = link->id; link->info.session_id = impl->this.session->obj.id; link->info.output_endpoint_id = outendpoint->info->id; link->info.output_stream_id = outstream ? outstream->info->id : SPA_ID_INVALID; link->info.input_endpoint_id = inendpoint->info->id; link->info.input_stream_id = instream ? instream->info->id : SPA_ID_INVALID; link->info.change_mask = PW_ENDPOINT_LINK_CHANGE_MASK_STATE | PW_ENDPOINT_LINK_CHANGE_MASK_PROPS; link->info.state = PW_ENDPOINT_LINK_STATE_ACTIVE; link->info.props = (struct spa_dict*) dict; } /* link the nodes, record the link proxies in the endpoint_link */ if (outnode != NULL && innode != NULL) res = link_nodes(impl, link, outnode, innode); else res = 0; if (link != NULL) { /* now create the endpoint link */ pw_client_session_link_update(impl->this.client_session, link->id, PW_CLIENT_SESSION_UPDATE_INFO, 0, NULL, &link->info); } return res; } int sm_media_session_remove_links(struct sm_media_session *sess, const struct spa_dict *dict) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); struct sm_object *obj; struct sm_node *outnode = NULL, *innode = NULL; const char *str; struct link *l, *t; /* find output node */ if ((str = spa_dict_lookup(dict, PW_KEY_LINK_OUTPUT_NODE)) != NULL && (obj = find_object(impl, atoi(str), PW_TYPE_INTERFACE_Node)) != NULL) outnode = (struct sm_node*)obj; /* find input node */ if ((str = spa_dict_lookup(dict, PW_KEY_LINK_INPUT_NODE)) != NULL && (obj = find_object(impl, atoi(str), PW_TYPE_INTERFACE_Node)) != NULL) innode = (struct sm_node*)obj; if (innode == NULL || outnode == NULL) return -EINVAL; spa_list_for_each_safe(l, t, &impl->link_list, link) { if (l->output_node == outnode->obj.id && l->input_node == innode->obj.id) { pw_proxy_destroy(l->proxy); } } return 0; } int sm_media_session_load_conf(struct sm_media_session *sess, const char *name, struct pw_properties *conf) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); return pw_conf_load_conf(impl->config_dir, name, conf); } int sm_media_session_load_state(struct sm_media_session *sess, const char *name, struct pw_properties *props) { return pw_conf_load_state(SESSION_PREFIX, name, props); } int sm_media_session_save_state(struct sm_media_session *sess, const char *name, const struct pw_properties *props) { return pw_conf_save_state(SESSION_PREFIX, name, props); } char *sm_media_session_sanitize_name(char *name, int size, char sub, const char *fmt, ...) { char *p; va_list varargs; int res; va_start(varargs, fmt); res = vsnprintf(name, size, fmt, varargs); va_end(varargs); if (res < 0) return NULL; for (p = name; *p; p++) { switch(*p) { case '0' ... '9': case 'a' ... 'z': case 'A' ... 'Z': case '.': case '-': case '_': break; default: *p = sub; break; } } return name; } char *sm_media_session_sanitize_description(char *name, int size, char sub, const char *fmt, ...) { char *p; va_list varargs; int res; va_start(varargs, fmt); res = vsnprintf(name, size, fmt, varargs); va_end(varargs); if (res < 0) return NULL; for (p = name; *p; p++) { switch(*p) { case ':': *p = sub; break; } } return name; } int sm_media_session_seat_active_changed(struct sm_media_session *sess, bool active) { struct impl *impl = SPA_CONTAINER_OF(sess, struct impl, this); if (active != impl->seat_active) { impl->seat_active = active; sm_media_session_emit_seat_active(impl, active); } return 0; } static void monitor_core_done(void *data, uint32_t id, int seq) { struct impl *impl = data; if (id == 0) handle_postponed_registry_events(impl, seq); if (seq == impl->monitor_seq) { pw_log_debug("%p: monitor sync stop %d", impl, seq); pw_core_set_paused(impl->policy_core, false); } } static const struct pw_core_events monitor_core_events = { PW_VERSION_CORE_EVENTS, .done = monitor_core_done, }; static int start_session(struct impl *impl) { impl->monitor_core = pw_context_connect(impl->this.context, NULL, 0); if (impl->monitor_core == NULL) { pw_log_error("can't start monitor: %m"); return -errno; } pw_core_add_listener(impl->monitor_core, &impl->monitor_listener, &monitor_core_events, impl); impl->monitor_registry = pw_core_get_registry(impl->monitor_core, PW_VERSION_REGISTRY, 0); pw_registry_add_listener(impl->monitor_registry, &impl->monitor_registry_listener, &monitor_registry_events, impl); return 0; } static void core_info(void *data, const struct pw_core_info *info) { struct impl *impl = data; pw_log_debug("%p: info", impl); impl->this.info = pw_core_info_merge(impl->this.info, info, true); if (impl->this.info->change_mask != 0) sm_media_session_emit_info(impl, impl->this.info); impl->this.info->change_mask = 0; } static void core_done(void *data, uint32_t id, int seq) { struct impl *impl = data; struct sync *s, *t; impl->last_seq = seq; spa_list_for_each_safe(s, t, &impl->sync_list, link) { if (s->seq == seq) { spa_list_remove(&s->link); s->callback(s->data); free(s); } } if (impl->rescan_seq == seq) { struct sm_object *obj, *to; if (!impl->scanning) { pw_log_trace("%p: rescan %u %d", impl, id, seq); impl->scanning = true; sm_media_session_emit_rescan(impl, seq); impl->scanning = false; if (impl->rescan_pending) { impl->rescan_pending = false; sm_media_session_schedule_rescan(&impl->this); } } spa_list_for_each_safe(obj, to, &impl->object_list, link) { if (obj->id == SPA_ID_INVALID) continue; pw_log_trace("%p: obj %p %08x", impl, obj, obj->changed); if (obj->changed) sm_object_emit_update(obj); obj->changed = 0; } } } static void core_error(void *data, uint32_t id, int seq, int res, const char *message) { struct impl *impl = data; pw_log(res == -ENOENT || res == -EINVAL ? SPA_LOG_LEVEL_INFO : SPA_LOG_LEVEL_WARN, "error id:%u seq:%d res:%d (%s): %s", id, seq, res, spa_strerror(res), message); if (id == PW_ID_CORE) { if (res == -EPIPE) pw_main_loop_quit(impl->loop); } } static const struct pw_core_events policy_core_events = { PW_VERSION_CORE_EVENTS, .info = core_info, .done = core_done, .error = core_error }; static void policy_core_destroy(void *data) { struct impl *impl = data; pw_log_debug("%p: policy core destroy", impl); impl->policy_core = NULL; } static const struct pw_proxy_events proxy_core_events = { PW_VERSION_PROXY_EVENTS, .destroy = policy_core_destroy, }; static int start_policy(struct impl *impl) { impl->policy_core = pw_context_connect(impl->this.context, NULL, 0); if (impl->policy_core == NULL) { pw_log_error("can't start policy: %m"); return -errno; } pw_core_add_listener(impl->policy_core, &impl->policy_listener, &policy_core_events, impl); pw_proxy_add_listener((struct pw_proxy*)impl->policy_core, &impl->proxy_policy_listener, &proxy_core_events, impl); impl->registry = pw_core_get_registry(impl->policy_core, PW_VERSION_REGISTRY, 0); pw_registry_add_listener(impl->registry, &impl->registry_listener, ®istry_events, impl); return 0; } static void session_shutdown(struct impl *impl) { struct sm_object *obj; struct registry_event *re; struct spa_list free_list; pw_log_info("%p", impl); sm_media_session_emit_shutdown(impl); /* * Monitors may still hold references to objects, which they * drop in session destroy event, so don't free undiscarded * objects yet. Destroy event handlers may remove any objects * in the list, so iterate carefully. */ spa_list_init(&free_list); spa_list_consume(obj, &impl->object_list, link) { if (obj->destroyed) { spa_list_remove(&obj->link); spa_list_append(&free_list, &obj->link); } else { sm_object_destroy_maybe_free(obj); } } spa_list_consume(re, &impl->registry_event_list, link) registry_event_free(re); impl->this.metadata = NULL; sm_media_session_emit_destroy(impl); spa_list_consume(obj, &free_list, link) sm_object_destroy(obj); spa_list_consume(obj, &impl->object_list, link) sm_object_destroy(obj); /* in case emit_destroy created new objects */ if (impl->registry) { spa_hook_remove(&impl->registry_listener); pw_proxy_destroy((struct pw_proxy*)impl->registry); } if (impl->monitor_registry) { spa_hook_remove(&impl->monitor_registry_listener); pw_proxy_destroy((struct pw_proxy*)impl->monitor_registry); } if (impl->policy_core) { spa_hook_remove(&impl->policy_listener); spa_hook_remove(&impl->proxy_policy_listener); pw_core_disconnect(impl->policy_core); } if (impl->monitor_core) { spa_hook_remove(&impl->monitor_listener); pw_core_disconnect(impl->monitor_core); } if (impl->this.info) pw_core_info_free(impl->this.info); } static int sm_metadata_start(struct sm_media_session *sess) { sess->metadata = sm_media_session_export_metadata(sess, "default"); if (sess->metadata == NULL) return -errno; return 0; } static int sm_pulse_bridge_start(struct sm_media_session *sess) { if (pw_context_load_module(sess->context, "libpipewire-module-protocol-pulse", NULL, NULL) == NULL) return -errno; return 0; } static void dbus_connection_disconnected(void *data) { struct impl *impl = data; pw_log_info("DBus disconnected"); sm_media_session_emit_dbus_disconnected(impl); } static const struct spa_dbus_connection_events dbus_connection_events = { SPA_VERSION_DBUS_CONNECTION_EVENTS, .disconnected = dbus_connection_disconnected }; static void do_quit(void *data, int signal_number) { struct impl *impl = data; pw_main_loop_quit(impl->loop); } static int collect_modules(struct impl *impl, const char *str) { struct spa_json it[3]; char key[512], value[512]; const char *dir, *prefix = NULL, *val; char check_path[PATH_MAX]; struct stat statbuf; int count = 0; dir = getenv("MEDIA_SESSION_CONFIG_DIR"); if (dir == NULL) { prefix = SESSION_PREFIX; if ((dir = getenv("PIPEWIRE_CONFIG_DIR")) == NULL) dir = MEDIA_SESSION_CONFDATADIR; } if (dir == NULL) return -ENOENT; again: spa_json_init(&it[0], str, strlen(str)); if (spa_json_enter_object(&it[0], &it[1]) < 0) return -EINVAL; while (spa_json_get_string(&it[1], key, sizeof(key)-1) > 0) { bool add = false; if (pw_properties_get(impl->modules, key) != NULL) { add = true; } else { snprintf(check_path, sizeof(check_path), "%s%s%s/%s", dir, prefix ? "/" : "", prefix ? prefix : "", key); add = (stat(check_path, &statbuf) == 0); } if (add) { if (spa_json_enter_array(&it[1], &it[2]) < 0) continue; while (spa_json_get_string(&it[2], value, sizeof(value)-1) > 0) { pw_properties_set(impl->modules, value, "true"); } } else if (spa_json_next(&it[1], &val) <= 0) break; } /* twice to resolve groups in module list */ if (count++ == 0) goto again; return 0; } static const struct { const char *name; const char *desc; int (*start)(struct sm_media_session *sess); const char *props; } modules[] = { { "flatpak", "manage flatpak access", sm_access_flatpak_start, NULL }, { "portal", "manage portal permissions", sm_access_portal_start, NULL }, { "metadata", "export metadata API", sm_metadata_start, NULL }, { "default-nodes", "restore default nodes", sm_default_nodes_start, NULL }, { "default-profile", "restore default profiles", sm_default_profile_start, NULL }, { "default-routes", "restore default route", sm_default_routes_start, NULL }, { "restore-stream", "restore stream settings", sm_restore_stream_start, NULL }, { "streams-follow-default", "move streams when default changes", sm_streams_follow_default_start, NULL }, { "alsa-no-dsp", "do not configure audio nodes in DSP mode", sm_alsa_no_dsp_start, NULL }, { "alsa-seq", "alsa seq midi support", sm_alsa_midi_start, NULL }, { "alsa-monitor", "alsa card udev detection", sm_alsa_monitor_start, NULL }, { "v4l2", "video for linux udev detection", sm_v4l2_monitor_start, NULL }, { "libcamera", "libcamera udev detection", sm_libcamera_monitor_start, NULL }, { "bluez5", "bluetooth support", sm_bluez5_monitor_start, NULL }, { "bluez5-autoswitch", "switch bluetooth profiles automatically", sm_bluez5_autoswitch_start, NULL }, { "suspend-node", "suspend inactive nodes", sm_suspend_node_start, NULL }, { "policy-node", "configure and link nodes", sm_policy_node_start, NULL }, { "pulse-bridge", "accept pulseaudio clients", sm_pulse_bridge_start, NULL }, #ifdef HAVE_SYSTEMD { "logind", "systemd-logind seat support", sm_logind_start, NULL }, #endif }; static bool is_module_enabled(struct impl *impl, const char *val) { return pw_properties_get_bool(impl->modules, val, false); } static void show_help(const char *name, struct impl *impl, const char *config_name) { size_t i; fprintf(stdout, "%s [options]\n" " -h, --help Show this help\n" " --version Show version\n" " -c, --config Load config (Default %s)\n", name, config_name); fprintf(stdout, "\noptions: (*=enabled)\n"); for (i = 0; i < SPA_N_ELEMENTS(modules); i++) { fprintf(stdout, "\t %c %-15.15s: %s\n", is_module_enabled(impl, modules[i].name) ? '*' : ' ', modules[i].name, modules[i].desc); } } int main(int argc, char *argv[]) { struct impl impl = { .seat_active = true }; const struct spa_support *support; const char *str, *config_name = SESSION_CONF; bool do_show_help = false; uint32_t n_support; int res = 0, c; static const struct option long_options[] = { { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'V' }, { "config", required_argument, NULL, 'c' }, { "verbose", no_argument, NULL, 'v' }, { NULL, 0, NULL, 0} }; size_t i; const struct spa_dict_item *item; enum spa_log_level level = pw_log_level; const char *config_dir; pw_init(&argc, &argv); PW_LOG_TOPIC_INIT(ms_topic); while ((c = getopt_long(argc, argv, "hVc:v", long_options, NULL)) != -1) { switch (c) { case 'v': if (level < SPA_LOG_LEVEL_TRACE) pw_log_set_level(++level); break; case 'h': do_show_help = true; break; case 'V': fprintf(stdout, "%s\n" "Compiled with libpipewire %s\n" "Linked with libpipewire %s\n", argv[0], pw_get_headers_version(), pw_get_library_version()); return 0; case 'c': config_name = optarg; break; default: return 1; } } config_dir = getenv("MEDIA_SESSION_CONFIG_DIR"); impl.config_dir = config_dir ? config_dir : SESSION_PREFIX; impl.this.props = pw_properties_new( PW_KEY_CONFIG_PREFIX, impl.config_dir, PW_KEY_CONFIG_NAME, config_name, NULL); if (impl.this.props == NULL) return 1; if ((impl.conf = pw_properties_new(NULL, NULL)) == NULL) return 1; pw_conf_load_conf(impl.config_dir, config_name, impl.conf); if ((str = pw_properties_get(impl.conf, "context.properties")) != NULL) pw_properties_update_string(impl.this.props, str, strlen(str)); if ((impl.modules = pw_properties_new("default", "true", NULL)) == NULL) return 1; if ((str = pw_properties_get(impl.conf, "session.modules")) != NULL) collect_modules(&impl, str); if (do_show_help) { show_help(argv[0], &impl, config_name); return 0; } pw_log_info("media-session context properties:"); spa_dict_for_each(item, &impl.this.props->dict) pw_log_info(" '%s' = '%s'", item->key, item->value); impl.loop = pw_main_loop_new(NULL); if (impl.loop == NULL) return 1; impl.this.loop = pw_main_loop_get_loop(impl.loop); pw_loop_add_signal(impl.this.loop, SIGINT, do_quit, &impl); pw_loop_add_signal(impl.this.loop, SIGTERM, do_quit, &impl); impl.this.context = pw_context_new(impl.this.loop, pw_properties_copy(impl.this.props), 0); if (impl.this.context == NULL) return 1; pw_context_set_object(impl.this.context, SM_TYPE_MEDIA_SESSION, &impl); pw_map_init(&impl.globals, 64, 64); spa_list_init(&impl.object_list); spa_list_init(&impl.registry_event_list); spa_list_init(&impl.link_list); pw_map_init(&impl.endpoint_links, 64, 64); spa_list_init(&impl.endpoint_link_list); spa_list_init(&impl.sync_list); spa_hook_list_init(&impl.hooks); support = pw_context_get_support(impl.this.context, &n_support); impl.dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus); if (impl.dbus) { impl.this.dbus_connection = spa_dbus_get_connection(impl.dbus, SPA_DBUS_TYPE_SESSION); if (impl.this.dbus_connection == NULL) pw_log_warn("no dbus connection"); else { pw_log_debug("got dbus connection %p", impl.this.dbus_connection); spa_dbus_connection_add_listener(impl.this.dbus_connection, &impl.dbus_connection_listener, &dbus_connection_events, &impl); } } else { pw_log_info("dbus disabled"); } if ((res = start_session(&impl)) < 0) goto exit; if ((res = start_policy(&impl)) < 0) goto exit; for (i = 0; i < SPA_N_ELEMENTS(modules); i++) { const char *name = modules[i].name; if (is_module_enabled(&impl, name)) { pw_log_info("enabling media session module: %s", name); modules[i].start(&impl.this); } } // sm_session_manager_start(&impl.this); pw_main_loop_run(impl.loop); exit: session_shutdown(&impl); pw_context_destroy(impl.this.context); pw_main_loop_destroy(impl.loop); pw_map_clear(&impl.endpoint_links); pw_map_clear(&impl.globals); pw_properties_free(impl.this.props); pw_properties_free(impl.conf); pw_properties_free(impl.modules); pw_deinit(); return res; } 0707010000006D000081A40000000000000000000000016178A88C0000285C000000000000000000000000000000000000002800000000media-session-0.4.1/src/media-session.h/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #ifndef SM_MEDIA_SESSION_H #define SM_MEDIA_SESSION_H #include <spa/monitor/device.h> #include <pipewire/impl.h> #ifdef __cplusplus extern "C" { #endif #define SM_TYPE_MEDIA_SESSION PW_TYPE_INFO_OBJECT_BASE "SessionManager" #define SM_MAX_PARAMS 32 struct sm_media_session; struct sm_object_events { #define SM_VERSION_OBJECT_EVENTS 0 uint32_t version; void (*update) (void *data); void (*destroy) (void *data); void (*free) (void *data); }; struct sm_object_methods { #define SM_VERSION_OBJECT_METHODS 0 uint32_t version; int (*acquire) (void *data); int (*release) (void *data); }; struct sm_object { uint32_t id; const char *type; struct spa_list link; struct sm_media_session *session; #define SM_OBJECT_CHANGE_MASK_LISTENER (1<<1) #define SM_OBJECT_CHANGE_MASK_PROPERTIES (1<<2) #define SM_OBJECT_CHANGE_MASK_BIND (1<<3) #define SM_OBJECT_CHANGE_MASK_LAST (1<<8) uint32_t mask; /**< monitored info */ uint32_t avail; /**< available info */ uint32_t changed; /**< changed since last update */ struct pw_properties *props; /**< global properties */ struct pw_proxy *proxy; struct spa_hook proxy_listener; struct spa_hook object_listener; pw_destroy_t destroy; int pending; struct pw_proxy *handle; struct spa_hook handle_listener; struct spa_hook_list hooks; struct spa_callbacks methods; struct spa_list data; unsigned int monitor_global:1; /**< whether handle is from monitor core */ unsigned int destroyed:1; /**< whether proxies have been destroyed */ unsigned int discarded:1; /**< whether monitors hold no references */ }; int sm_object_add_listener(struct sm_object *obj, struct spa_hook *listener, const struct sm_object_events *events, void *data); #define sm_object_call(o,...) spa_callbacks_call(&(o)->methods, struct sm_object_methods, __VA_ARGS__) #define sm_object_call_res(o,...) spa_callbacks_call_res(&(o)->methods, struct sm_object_methods, 0, __VA_ARGS__) #define sm_object_acquire(o) sm_object_call(o, acquire, 0) #define sm_object_release(o) sm_object_call(o, release, 0) struct sm_param { uint32_t id; struct spa_list link; /**< link in param_list */ struct spa_pod *param; }; /** get user data with \a id and \a size to an object */ void *sm_object_add_data(struct sm_object *obj, const char *id, size_t size); void *sm_object_get_data(struct sm_object *obj, const char *id); int sm_object_remove_data(struct sm_object *obj, const char *id); int sm_object_sync_update(struct sm_object *obj); int sm_object_destroy(struct sm_object *obj); #define sm_object_discard(o) do { (o)->discarded = true; } while (0) struct sm_client { struct sm_object obj; #define SM_CLIENT_CHANGE_MASK_INFO (SM_OBJECT_CHANGE_MASK_LAST<<0) #define SM_CLIENT_CHANGE_MASK_PERMISSIONS (SM_OBJECT_CHANGE_MASK_LAST<<1) struct pw_client_info *info; }; struct sm_device { struct sm_object obj; unsigned int locked:1; /**< if the device is locked by someone else right now */ #define SM_DEVICE_CHANGE_MASK_INFO (SM_OBJECT_CHANGE_MASK_LAST<<0) #define SM_DEVICE_CHANGE_MASK_PARAMS (SM_OBJECT_CHANGE_MASK_LAST<<1) #define SM_DEVICE_CHANGE_MASK_NODES (SM_OBJECT_CHANGE_MASK_LAST<<2) uint32_t n_params; struct spa_list param_list; /**< list of sm_param */ int param_seq[SM_MAX_PARAMS]; struct pw_device_info *info; struct spa_list node_list; }; struct sm_node { struct sm_object obj; struct sm_device *device; /**< optional device */ struct spa_list link; /**< link in device node_list */ #define SM_NODE_CHANGE_MASK_INFO (SM_OBJECT_CHANGE_MASK_LAST<<0) #define SM_NODE_CHANGE_MASK_PARAMS (SM_OBJECT_CHANGE_MASK_LAST<<1) #define SM_NODE_CHANGE_MASK_PORTS (SM_OBJECT_CHANGE_MASK_LAST<<2) uint32_t n_params; struct spa_list param_list; /**< list of sm_param */ int param_seq[SM_MAX_PARAMS]; struct pw_node_info *info; struct spa_list port_list; char *target_node; /**< desired target node */ unsigned int fixed_target:1; /**< target_node has priority over node.target */ }; struct sm_port { struct sm_object obj; enum pw_direction direction; #define SM_PORT_TYPE_UNKNOWN 0 #define SM_PORT_TYPE_DSP_AUDIO 1 #define SM_PORT_TYPE_DSP_MIDI 2 uint32_t type; uint32_t channel; struct sm_node *node; struct spa_list link; /**< link in node port_list */ #define SM_PORT_CHANGE_MASK_INFO (SM_OBJECT_CHANGE_MASK_LAST<<0) struct pw_port_info *info; unsigned int visited:1; }; struct sm_session { struct sm_object obj; #define SM_SESSION_CHANGE_MASK_INFO (SM_OBJECT_CHANGE_MASK_LAST<<0) #define SM_SESSION_CHANGE_MASK_ENDPOINTS (SM_OBJECT_CHANGE_MASK_LAST<<1) struct pw_session_info *info; struct spa_list endpoint_list; }; struct sm_endpoint { struct sm_object obj; int32_t priority; struct sm_session *session; struct spa_list link; /**< link in session endpoint_list */ #define SM_ENDPOINT_CHANGE_MASK_INFO (SM_OBJECT_CHANGE_MASK_LAST<<0) #define SM_ENDPOINT_CHANGE_MASK_STREAMS (SM_OBJECT_CHANGE_MASK_LAST<<1) struct pw_endpoint_info *info; struct spa_list stream_list; }; struct sm_endpoint_stream { struct sm_object obj; int32_t priority; struct sm_endpoint *endpoint; struct spa_list link; /**< link in endpoint stream_list */ struct spa_list link_list; /**< list of links */ #define SM_ENDPOINT_STREAM_CHANGE_MASK_INFO (SM_OBJECT_CHANGE_MASK_LAST<<0) struct pw_endpoint_stream_info *info; }; struct sm_endpoint_link { struct sm_object obj; struct spa_list link; /**< link in session link_list */ struct spa_list output_link; struct sm_endpoint_stream *output; struct spa_list input_link; struct sm_endpoint_stream *input; #define SM_ENDPOINT_LINK_CHANGE_MASK_INFO (SM_OBJECT_CHANGE_MASK_LAST<<0) struct pw_endpoint_link_info *info; }; struct sm_media_session_events { #define SM_VERSION_MEDIA_SESSION_EVENTS 0 uint32_t version; void (*info) (void *data, const struct pw_core_info *info); void (*create) (void *data, struct sm_object *object); void (*remove) (void *data, struct sm_object *object); void (*rescan) (void *data, int seq); void (*shutdown) (void *data); void (*destroy) (void *data); void (*seat_active) (void *data, bool active); void (*dbus_disconnected) (void *data); }; struct sm_media_session { struct sm_session *session; /** session object managed by this session */ struct pw_properties *props; uint32_t session_id; struct pw_client_session *client_session; struct pw_loop *loop; /** the main loop */ struct pw_context *context; struct spa_dbus_connection *dbus_connection; struct pw_metadata *metadata; struct pw_core_info *info; }; int sm_media_session_add_listener(struct sm_media_session *sess, struct spa_hook *listener, const struct sm_media_session_events *events, void *data); int sm_media_session_roundtrip(struct sm_media_session *sess); int sm_media_session_sync(struct sm_media_session *sess, void (*callback) (void *data), void *data); struct sm_object *sm_media_session_find_object(struct sm_media_session *sess, uint32_t id); int sm_media_session_destroy_object(struct sm_media_session *sess, uint32_t id); int sm_media_session_for_each_object(struct sm_media_session *sess, int (*callback) (void *data, struct sm_object *object), void *data); int sm_media_session_schedule_rescan(struct sm_media_session *sess); struct pw_metadata *sm_media_session_export_metadata(struct sm_media_session *sess, const char *name); struct pw_proxy *sm_media_session_export(struct sm_media_session *sess, const char *type, const struct spa_dict *props, void *object, size_t user_data_size); struct sm_node *sm_media_session_export_node(struct sm_media_session *sess, const struct spa_dict *props, struct pw_impl_node *node); struct sm_device *sm_media_session_export_device(struct sm_media_session *sess, const struct spa_dict *props, struct spa_device *device); struct pw_proxy *sm_media_session_create_object(struct sm_media_session *sess, const char *factory_name, const char *type, uint32_t version, const struct spa_dict *props, size_t user_data_size); struct sm_node *sm_media_session_create_node(struct sm_media_session *sess, const char *factory_name, const struct spa_dict *props); int sm_media_session_create_links(struct sm_media_session *sess, const struct spa_dict *dict); int sm_media_session_remove_links(struct sm_media_session *sess, const struct spa_dict *dict); int sm_media_session_load_conf(struct sm_media_session *sess, const char *name, struct pw_properties *conf); int sm_media_session_load_state(struct sm_media_session *sess, const char *name, struct pw_properties *props); int sm_media_session_save_state(struct sm_media_session *sess, const char *name, const struct pw_properties *props); int sm_media_session_match_rules(const char *rules, size_t size, struct pw_properties *props); char *sm_media_session_sanitize_name(char *name, int size, char sub, const char *fmt, ...) SPA_PRINTF_FUNC(4, 5); char *sm_media_session_sanitize_description(char *name, int size, char sub, const char *fmt, ...) SPA_PRINTF_FUNC(4, 5); int sm_media_session_seat_active_changed(struct sm_media_session *sess, bool active); #ifdef __cplusplus } #endif #endif 0707010000006E000081A40000000000000000000000016178A88C00000403000000000000000000000000000000000000002400000000media-session-0.4.1/src/meson.buildsm_logind_src = [] sm_logind_dep = [] if systemd.found() and systemd_dep.found() sm_logind_src = ['logind.c'] sm_logind_dep = [systemd_dep] endif media_session_sources = [ 'access-flatpak.c', 'access-portal.c', 'alsa-no-dsp.c', 'alsa-midi.c', 'alsa-monitor.c', 'alsa-endpoint.c', 'bluez-monitor.c', 'bluez-endpoint.c', 'bluez-autoswitch.c', 'default-nodes.c', 'default-profile.c', 'default-routes.c', 'media-session.c', 'session-manager.c', 'match-rules.c', 'metadata.c', 'stream-endpoint.c', 'restore-stream.c', 'policy-ep.c', 'policy-node.c', 'reserve.c', 'streams-follow-default.c', 'v4l2-monitor.c', 'v4l2-endpoint.c', 'libcamera-monitor.c', 'suspend-node.c', ] + sm_logind_src pipewire_media_session = executable('pipewire-media-session', media_session_sources, include_directories: [configinc], install: true, dependencies : [dbus_dep, pipewire_dep, alsa_dep, mathlib, sm_logind_dep, libinotify_dep], ) 0707010000006F000081A40000000000000000000000016178A88C00000BAA000000000000000000000000000000000000002300000000media-session-0.4.1/src/metadata.c/* Metadata API * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include "pipewire/pipewire.h" #include "pipewire/array.h" #include <spa/utils/string.h> #include <pipewire/extensions/metadata.h> #include "media-session.h" /** \page page_media_session_module_metadata Media Session Module: Metadata */ #define NAME "metadata" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct metadata { struct pw_impl_metadata *impl; struct pw_metadata *metadata; struct sm_media_session *session; struct spa_hook session_listener; struct pw_proxy *proxy; }; static void session_destroy(void *data) { struct metadata *this = data; spa_hook_remove(&this->session_listener); pw_proxy_destroy(this->proxy); pw_impl_metadata_destroy(this->impl); free(this); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .destroy = session_destroy, }; struct pw_metadata *sm_media_session_export_metadata(struct sm_media_session *sess, const char *name) { struct metadata *this; int res; struct spa_dict_item items[1]; PW_LOG_TOPIC_INIT(mod_topic); this = calloc(1, sizeof(*this)); if (this == NULL) goto error_errno; this->impl = pw_context_create_metadata(sess->context, name, NULL, 0); if (this->impl == NULL) goto error_errno; this->metadata = pw_impl_metadata_get_implementation(this->impl); items[0] = SPA_DICT_ITEM_INIT(PW_KEY_METADATA_NAME, name); this->session = sess; this->proxy = sm_media_session_export(sess, PW_TYPE_INTERFACE_Metadata, &SPA_DICT_INIT_ARRAY(items), this->metadata, 0); if (this->proxy == NULL) goto error_errno; sm_media_session_add_listener(sess, &this->session_listener, &session_events, this); return this->metadata; error_errno: res = -errno; goto error_free; error_free: free(this); errno = -res; return NULL; } 07070100000070000081A40000000000000000000000016178A88C00003701000000000000000000000000000000000000002400000000media-session-0.4.1/src/policy-ep.c/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/utils/string.h> #include <spa/param/audio/format-utils.h> #include <spa/param/props.h> #include <spa/debug/pod.h> #include "pipewire/pipewire.h" #include "pipewire/extensions/session-manager.h" #include "media-session.h" /** \page page_media_session_module_policy_endpoint Media Session Module: Policy Endpoint */ #define NAME "policy-ep" #define SESSION_KEY "policy-endpoint" #define DEFAULT_CHANNELS 2 #define DEFAULT_SAMPLERATE 48000 #define DEFAULT_IDLE_SECONDS 3 PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct impl { struct timespec now; struct sm_media_session *session; struct spa_hook listener; struct pw_context *context; struct spa_list endpoint_list; int seq; }; struct endpoint { struct sm_endpoint *obj; uint32_t id; struct impl *impl; struct spa_list link; /**< link in impl endpoint_list */ enum pw_direction direction; uint32_t linked; uint32_t client_id; int32_t priority; #define ENDPOINT_TYPE_UNKNOWN 0 #define ENDPOINT_TYPE_STREAM 1 #define ENDPOINT_TYPE_DEVICE 2 uint32_t type; char *media; uint32_t media_type; uint32_t media_subtype; struct spa_audio_info_raw format; uint64_t plugged; unsigned int exclusive:1; unsigned int enabled:1; unsigned int busy:1; }; struct stream { struct sm_endpoint_stream *obj; uint32_t id; struct impl *impl; struct endpoint *endpoint; }; static int handle_endpoint(struct impl *impl, struct sm_object *object) { const char *media_class; enum pw_direction direction; struct endpoint *ep; uint32_t client_id = SPA_ID_INVALID; if (object->props) { pw_properties_fetch_uint32(object->props, PW_KEY_CLIENT_ID, &client_id); } media_class = object->props ? pw_properties_get(object->props, PW_KEY_MEDIA_CLASS) : NULL; pw_log_debug("%p: endpoint "PW_KEY_MEDIA_CLASS" %s", impl, media_class); if (media_class == NULL) return 0; ep = sm_object_add_data(object, SESSION_KEY, sizeof(struct endpoint)); ep->obj = (struct sm_endpoint*)object; ep->id = object->id; ep->impl = impl; ep->client_id = client_id; ep->type = ENDPOINT_TYPE_UNKNOWN; ep->enabled = true; spa_list_append(&impl->endpoint_list, &ep->link); if (spa_strstartswith(media_class, "Stream/")) { media_class += strlen("Stream/"); if (spa_strstartswith(media_class, "Output/")) { direction = PW_DIRECTION_OUTPUT; media_class += strlen("Output/"); } else if (spa_strstartswith(media_class, "Input/")) { direction = PW_DIRECTION_INPUT; media_class += strlen("Input/"); } else return 0; ep->direction = direction; ep->type = ENDPOINT_TYPE_STREAM; ep->media = strdup(media_class); pw_log_debug("%p: endpoint %d is stream %s", impl, object->id, ep->media); } else { const char *media; if (spa_strstartswith(media_class, "Audio/")) { media_class += strlen("Audio/"); media = "Audio"; } else if (spa_strstartswith(media_class, "Video/")) { media_class += strlen("Video/"); media = "Video"; } else return 0; if (spa_streq(media_class, "Sink")) direction = PW_DIRECTION_INPUT; else if (spa_streq(media_class, "Source")) direction = PW_DIRECTION_OUTPUT; else return 0; ep->direction = direction; ep->type = ENDPOINT_TYPE_DEVICE; ep->media = strdup(media); pw_log_debug("%p: endpoint %d '%s' prio:%d", impl, object->id, ep->media, ep->priority); } return 1; } static void destroy_endpoint(struct impl *impl, struct endpoint *ep) { spa_list_remove(&ep->link); free(ep->media); sm_object_remove_data((struct sm_object*)ep->obj, SESSION_KEY); } static int handle_stream(struct impl *impl, struct sm_object *object) { struct sm_endpoint_stream *stream = (struct sm_endpoint_stream*)object; struct stream *s; struct endpoint *ep; if (stream->endpoint == NULL) return 0; ep = sm_object_get_data(&stream->endpoint->obj, SESSION_KEY); if (ep == NULL) return 0; s = sm_object_add_data(object, SESSION_KEY, sizeof(struct stream)); s->obj = (struct sm_endpoint_stream*)object; s->id = object->id; s->impl = impl; s->endpoint = ep; return 0; } static void destroy_stream(struct impl *impl, struct stream *s) { sm_object_remove_data((struct sm_object*)s->obj, SESSION_KEY); } static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; int res; if (spa_streq(object->type, PW_TYPE_INTERFACE_Endpoint)) res = handle_endpoint(impl, object); else if (spa_streq(object->type, PW_TYPE_INTERFACE_EndpointStream)) res = handle_stream(impl, object); else res = 0; if (res < 0) { pw_log_warn("%p: can't handle global %d", impl, object->id); } else sm_media_session_schedule_rescan(impl->session); } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; pw_log_debug("%p: remove global '%d'", impl, object->id); if (spa_streq(object->type, PW_TYPE_INTERFACE_Endpoint)) { struct endpoint *ep; if ((ep = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_endpoint(impl, ep); } else if (spa_streq(object->type, PW_TYPE_INTERFACE_EndpointStream)) { struct stream *s; if ((s = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_stream(impl, s); } sm_media_session_schedule_rescan(impl->session); } struct find_data { struct impl *impl; struct endpoint *ep; struct endpoint *endpoint; bool exclusive; int priority; uint64_t plugged; }; static int find_endpoint(void *data, struct endpoint *endpoint) { struct find_data *find = data; struct impl *impl = find->impl; int priority = 0; uint64_t plugged = 0; pw_log_debug("%p: looking at endpoint '%d' enabled:%d busy:%d exclusive:%d", impl, endpoint->id, endpoint->enabled, endpoint->busy, endpoint->exclusive); if (!endpoint->enabled) return 0; if (endpoint->direction == find->ep->direction) { pw_log_debug(".. same direction"); return 0; } if (!spa_streq(endpoint->media, find->ep->media)) { pw_log_debug(".. incompatible media %s <-> %s", endpoint->media, find->ep->media); return 0; } plugged = endpoint->plugged; priority = endpoint->priority; if ((find->exclusive && endpoint->busy) || endpoint->exclusive) { pw_log_debug("%p: endpoint '%d' in use", impl, endpoint->id); return 0; } pw_log_debug("%p: found endpoint '%d' %"PRIu64" prio:%d", impl, endpoint->id, plugged, priority); if (find->endpoint == NULL || priority > find->priority || (priority == find->priority && plugged > find->plugged)) { pw_log_debug("%p: new best %d %" PRIu64, impl, priority, plugged); find->endpoint = endpoint; find->priority = priority; find->plugged = plugged; } return 0; } static int link_endpoints(struct endpoint *endpoint, struct endpoint *peer) { struct impl *impl = endpoint->impl; struct pw_properties *props; pw_log_debug("%p: link endpoints %d %d", impl, endpoint->id, peer->id); if (endpoint->direction == PW_DIRECTION_INPUT) { struct endpoint *t = endpoint; endpoint = peer; peer = t; } props = pw_properties_new(NULL, NULL); pw_properties_setf(props, PW_KEY_ENDPOINT_LINK_OUTPUT_ENDPOINT, "%d", endpoint->id); pw_properties_setf(props, PW_KEY_ENDPOINT_LINK_OUTPUT_STREAM, "%d", -1); pw_properties_setf(props, PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT, "%d", peer->id); pw_properties_setf(props, PW_KEY_ENDPOINT_LINK_INPUT_STREAM, "%d", -1); pw_log_debug("%p: endpoint %d -> endpoint %d", impl, endpoint->id, peer->id); pw_endpoint_create_link((struct pw_endpoint*)endpoint->obj->obj.proxy, &props->dict); pw_properties_free(props); endpoint->linked++; peer->linked++; return 0; } static int link_node(struct endpoint *endpoint, struct sm_node *peer) { struct impl *impl = endpoint->impl; struct pw_properties *props; pw_log_debug("%p: link endpoint %d to node %d", impl, endpoint->id, peer->obj.id); props = pw_properties_new(NULL, NULL); if (endpoint->direction == PW_DIRECTION_INPUT) { pw_properties_setf(props, PW_KEY_LINK_OUTPUT_NODE, "%d", peer->obj.id); pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%d", -1); pw_properties_setf(props, PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT, "%d", endpoint->id); pw_properties_setf(props, PW_KEY_ENDPOINT_LINK_INPUT_STREAM, "%d", -1); pw_log_debug("%p: node %d -> endpoint %d", impl, peer->obj.id, endpoint->id); } else { pw_properties_setf(props, PW_KEY_ENDPOINT_LINK_OUTPUT_ENDPOINT, "%d", endpoint->id); pw_properties_setf(props, PW_KEY_ENDPOINT_LINK_OUTPUT_STREAM, "%d", -1); pw_properties_setf(props, PW_KEY_LINK_INPUT_NODE, "%d", peer->obj.id); pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%d", -1); pw_log_debug("%p: endpoint %d -> node %d", impl, endpoint->id, peer->obj.id); } pw_endpoint_create_link((struct pw_endpoint*)endpoint->obj->obj.proxy, &props->dict); pw_properties_free(props); endpoint->linked++; return 0; } static int rescan_endpoint(struct impl *impl, struct endpoint *ep) { struct spa_dict *props; const char *str; bool exclusive; struct find_data find; struct pw_endpoint_info *info; struct endpoint *peer; struct sm_object *obj; struct sm_node *node; if (ep->type == ENDPOINT_TYPE_DEVICE) return 0; if (ep->obj->info == NULL || ep->obj->info->props == NULL) { pw_log_debug("%p: endpoint %d has no properties", impl, ep->id); return 0; } if (ep->linked > 0) { pw_log_debug("%p: endpoint %d is already linked", impl, ep->id); return 0; } info = ep->obj->info; props = info->props; str = spa_dict_lookup(props, PW_KEY_ENDPOINT_AUTOCONNECT); if (str == NULL || !pw_properties_parse_bool(str)) { pw_log_debug("%p: endpoint %d does not need autoconnect", impl, ep->id); return 0; } if (ep->media == NULL) { pw_log_debug("%p: endpoint %d has unknown media", impl, ep->id); return 0; } spa_zero(find); if ((str = spa_dict_lookup(props, PW_KEY_NODE_EXCLUSIVE)) != NULL) exclusive = pw_properties_parse_bool(str); else exclusive = false; find.impl = impl; find.ep = ep; find.exclusive = exclusive; pw_log_debug("%p: exclusive:%d", impl, exclusive); str = spa_dict_lookup(props, PW_KEY_ENDPOINT_TARGET); if (str == NULL) str = spa_dict_lookup(props, PW_KEY_NODE_TARGET); if (str != NULL) { uint32_t path_id = atoi(str); pw_log_debug("%p: target:%d", impl, path_id); if ((obj = sm_media_session_find_object(impl->session, path_id)) != NULL) { if (spa_streq(obj->type, PW_TYPE_INTERFACE_Endpoint)) { if ((peer = sm_object_get_data(obj, SESSION_KEY)) != NULL) goto do_link; } else if (spa_streq(obj->type, PW_TYPE_INTERFACE_Node)) { node = (struct sm_node*)obj; goto do_link_node; } } } spa_list_for_each(peer, &impl->endpoint_list, link) find_endpoint(&find, peer); if (find.endpoint == NULL) { struct sm_object *obj; pw_log_warn("%p: no endpoint found for %d", impl, ep->id); str = spa_dict_lookup(props, PW_KEY_NODE_DONT_RECONNECT); if (str != NULL && pw_properties_parse_bool(str)) { // pw_registry_destroy(impl->registry, ep->id); } obj = sm_media_session_find_object(impl->session, ep->client_id); if (obj && spa_streq(obj->type, PW_TYPE_INTERFACE_Client)) { pw_client_error((struct pw_client*)obj->proxy, ep->id, -ENOENT, "no endpoint available"); } return -ENOENT; } peer = find.endpoint; if (exclusive && peer->busy) { pw_log_warn("%p: endpoint %d busy, can't get exclusive access", impl, peer->id); return -EBUSY; } peer->exclusive = exclusive; pw_log_debug("%p: linking to endpoint '%d'", impl, peer->id); peer->busy = true; do_link: link_endpoints(ep, peer); return 1; do_link_node: link_node(ep, node); return 1; } static void session_rescan(void *data, int seq) { struct impl *impl = data; struct endpoint *ep; clock_gettime(CLOCK_MONOTONIC, &impl->now); pw_log_debug("%p: rescan", impl); spa_list_for_each(ep, &impl->endpoint_list, link) rescan_endpoint(impl, ep); } static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->listener); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .rescan = session_rescan, .destroy = session_destroy, }; int sm_policy_ep_start(struct sm_media_session *session) { struct impl *impl; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; impl->context = session->context; spa_list_init(&impl->endpoint_list); sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); return 0; } 07070100000071000081A40000000000000000000000016178A88C000099F4000000000000000000000000000000000000002600000000media-session-0.4.1/src/policy-node.c/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/utils/string.h> #include <spa/param/audio/format-utils.h> #include <spa/param/props.h> #include <spa/debug/pod.h> #include <spa/pod/filter.h> #include <spa/utils/json.h> #include "pipewire/pipewire.h" #include "pipewire/extensions/metadata.h" #include "media-session.h" /** \page page_media_session_module_policy_node Media Session Module: Policy Node */ #define NAME "policy-node" #define SESSION_KEY "policy-node" #define DEFAULT_IDLE_SECONDS 3 #define DEFAULT_AUDIO_SINK_KEY "default.audio.sink" #define DEFAULT_AUDIO_SOURCE_KEY "default.audio.source" #define DEFAULT_VIDEO_SOURCE_KEY "default.video.source" #define DEFAULT_CONFIG_AUDIO_SINK_KEY "default.configured.audio.sink" #define DEFAULT_CONFIG_AUDIO_SOURCE_KEY "default.configured.audio.source" #define DEFAULT_CONFIG_VIDEO_SOURCE_KEY "default.configured.video.source" #define DEFAULT_AUDIO_SINK 0 #define DEFAULT_AUDIO_SOURCE 1 #define DEFAULT_VIDEO_SOURCE 2 #define MAX_LINK_RETRY 5 PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct default_node { char *key; char *key_config; char *value; char *config; }; struct impl { struct timespec now; struct sm_media_session *session; struct spa_hook listener; struct spa_hook meta_listener; struct pw_context *context; uint32_t sample_rate; struct spa_list node_list; unsigned int node_list_changed:1; unsigned int linking_node_removed:1; int seq; struct default_node defaults[4]; bool streams_follow_default; bool alsa_no_dsp; }; struct node { struct sm_node *obj; uint32_t id; struct impl *impl; struct spa_list link; /**< link in impl node_list */ enum pw_direction direction; struct spa_hook listener; struct node *peer; struct node *failed_peer; uint32_t client_id; int32_t priority; #define NODE_TYPE_UNKNOWN 0 #define NODE_TYPE_STREAM 1 #define NODE_TYPE_DEVICE 2 uint32_t type; char *media; struct spa_audio_info format; int connect_count; int failed_count; uint64_t plugged; unsigned int active:1; unsigned int exclusive:1; unsigned int enabled:1; unsigned int configured:1; unsigned int dont_remix:1; unsigned int monitor:1; unsigned int capture_sink:1; unsigned int virtual:1; unsigned int linking:1; unsigned int have_passthrough:1; unsigned int passthrough_only:1; unsigned int passthrough:1; unsigned int want_passthrough:1; unsigned int unpositioned:1; }; static bool is_unpositioned(struct spa_audio_info *info) { uint32_t i; if (SPA_FLAG_IS_SET(info->info.raw.flags, SPA_AUDIO_FLAG_UNPOSITIONED)) return true; for (i = 0; i < info->info.raw.channels; i++) if (info->info.raw.position[i] >= SPA_AUDIO_CHANNEL_START_Aux && info->info.raw.position[i] <= SPA_AUDIO_CHANNEL_LAST_Aux) return true; return false; } static bool find_format(struct node *node) { struct impl *impl = node->impl; struct sm_param *p; bool have_format = false; node->have_passthrough = false; node->passthrough_only = false; spa_list_for_each(p, &node->obj->param_list, link) { struct spa_audio_info info = { 0, }; struct spa_pod *position = NULL; uint32_t n_position = 0; if (p->id != SPA_PARAM_EnumFormat) continue; if (spa_format_parse(p->param, &info.media_type, &info.media_subtype) < 0) continue; if (info.media_type != SPA_MEDIA_TYPE_audio) continue; switch (info.media_subtype) { case SPA_MEDIA_SUBTYPE_raw: spa_pod_object_fixate((struct spa_pod_object*)p->param); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, p->param); /* defaults */ info.info.raw.format = SPA_AUDIO_FORMAT_F32; info.info.raw.rate = impl->sample_rate; info.info.raw.channels = 2; info.info.raw.position[0] = SPA_AUDIO_CHANNEL_FL; info.info.raw.position[1] = SPA_AUDIO_CHANNEL_FR; spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_Format, NULL, SPA_FORMAT_AUDIO_format, SPA_POD_Id(&info.info.raw.format), SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info.info.raw.rate), SPA_FORMAT_AUDIO_channels, SPA_POD_Int(&info.info.raw.channels), SPA_FORMAT_AUDIO_position, SPA_POD_OPT_Pod(&position)); if (position != NULL) n_position = spa_pod_copy_array(position, SPA_TYPE_Id, info.info.raw.position, SPA_AUDIO_MAX_CHANNELS); if (n_position == 0 || n_position != info.info.raw.channels) SPA_FLAG_SET(info.info.raw.flags, SPA_AUDIO_FLAG_UNPOSITIONED); if (node->format.info.raw.channels < info.info.raw.channels) { node->format = info; if (is_unpositioned(&info)) node->unpositioned = true; } have_format = true; break; case SPA_MEDIA_SUBTYPE_iec958: case SPA_MEDIA_SUBTYPE_dsd: pw_log_info("passthrough node %d found", node->id); node->have_passthrough = true; break; } } if (!have_format && node->have_passthrough) { pw_log_info("passthrough only node %d found", node->id); node->passthrough_only = true; have_format = true; } return have_format; } static bool check_passthrough(struct node *node, struct node *peer) { struct sm_param *p1, *p2; char buffer[1024]; struct spa_pod_builder b; struct spa_pod *res; if (peer->obj->info == NULL) return false; if (peer->obj->info->state == PW_NODE_STATE_RUNNING) return false; if (!node->have_passthrough || !peer->have_passthrough) return false; spa_list_for_each(p1, &node->obj->param_list, link) { if (p1->id != SPA_PARAM_EnumFormat) continue; spa_list_for_each(p2, &peer->obj->param_list, link) { if (p2->id != SPA_PARAM_EnumFormat) continue; spa_pod_builder_init(&b, buffer, sizeof(buffer)); if (spa_pod_filter(&b, &res, p1->param, p2->param) >= 0) return true; } } return false; } static void ensure_suspended(struct node *node) { struct spa_command *cmd = &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Suspend); if (node->obj->info->state < PW_NODE_STATE_IDLE) return; pw_node_send_command((struct pw_node*)node->obj->obj.proxy, cmd); } static int configure_passthrough(struct node *node) { char buf[1024]; struct spa_pod_builder b = { 0, }; struct spa_pod *param; pw_log_info("node %d passthrough", node->id); ensure_suspended(node); spa_pod_builder_init(&b, buf, sizeof(buf)); param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(node->direction), SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_passthrough), SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(false)); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, param); pw_node_set_param((struct pw_node*)node->obj->obj.proxy, SPA_PARAM_PortConfig, 0, param); node->configured = true; node->passthrough = true; return 0; } static int configure_node(struct node *node, struct spa_audio_info *info, bool force) { struct impl *impl = node->impl; char buf[1024]; struct spa_pod_builder b = { 0, }; struct spa_pod *param; struct spa_audio_info format; enum pw_direction direction; uint32_t mode; if (node->configured && !force) { pw_log_debug("node %d is configured passthrough:%d", node->id, node->passthrough); return 0; } if (!spa_streq(node->media, "Audio")) return 0; ensure_suspended(node); format = node->format; if (impl->alsa_no_dsp) { if ((info != NULL && memcmp(&node->format, info, sizeof(node->format)) == 0) || node->type == NODE_TYPE_DEVICE) mode = SPA_PARAM_PORT_CONFIG_MODE_passthrough; else mode = SPA_PARAM_PORT_CONFIG_MODE_convert; } else { mode = SPA_PARAM_PORT_CONFIG_MODE_dsp; } if (mode != SPA_PARAM_PORT_CONFIG_MODE_passthrough && info != NULL && info->info.raw.channels > 0) { pw_log_info("node %d monitor:%d channelmix %d->%d", node->id, node->monitor, format.info.raw.channels, info->info.raw.channels); format = *info; } else { pw_log_info("node %d monitor:%d channelmix %d", node->id, node->monitor, format.info.raw.channels); } format.info.raw.rate = impl->sample_rate; if (node->virtual) direction = pw_direction_reverse(node->direction); else direction = node->direction; spa_pod_builder_init(&b, buf, sizeof(buf)); param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &format.info.raw); param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(direction), SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(mode), SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(true), SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, param); pw_node_set_param((struct pw_node*)node->obj->obj.proxy, SPA_PARAM_PortConfig, 0, param); node->configured = true; node->passthrough = false; if (node->type == NODE_TYPE_DEVICE) { /* Schedule rescan in case we need to move streams */ sm_media_session_schedule_rescan(impl->session); } return 0; } static void object_update(void *data) { struct node *node = data; struct impl *impl = node->impl; const char *str; pw_log_debug("%p: node %d %08x", impl, node->id, node->obj->obj.changed); if (node->obj->obj.avail & SM_NODE_CHANGE_MASK_INFO && node->obj->info != NULL && node->obj->info->props != NULL) { str = spa_dict_lookup(node->obj->info->props, PW_KEY_NODE_EXCLUSIVE); node->exclusive = str ? pw_properties_parse_bool(str) : false; } if (!node->active) { if (node->obj->obj.avail & SM_NODE_CHANGE_MASK_PARAMS) { if (!find_format(node)) { pw_log_debug("%p: node %d can't find format", impl, node->id); return; } node->active = true; } if (node->active) sm_media_session_schedule_rescan(impl->session); } } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static int handle_node(struct impl *impl, struct sm_object *object) { const char *media_class = NULL, *role; enum pw_direction direction; struct node *node; uint32_t client_id = SPA_ID_INVALID; if (object->props) { pw_properties_fetch_uint32(object->props, PW_KEY_CLIENT_ID, &client_id); media_class = pw_properties_get(object->props, PW_KEY_MEDIA_CLASS); role = pw_properties_get(object->props, PW_KEY_MEDIA_ROLE); } pw_log_debug("%p: node "PW_KEY_MEDIA_CLASS" %s", impl, media_class); if (media_class == NULL) return 0; node = sm_object_add_data(object, SESSION_KEY, sizeof(struct node)); node->obj = (struct sm_node*)object; node->id = object->id; node->impl = impl; node->client_id = client_id; node->type = NODE_TYPE_UNKNOWN; spa_list_append(&impl->node_list, &node->link); impl->node_list_changed = true; if (role && spa_streq(role, "DSP")) node->active = node->configured = true; if (spa_strstartswith(media_class, "Stream/")) { media_class += strlen("Stream/"); if (spa_strstartswith(media_class, "Output/")) { direction = PW_DIRECTION_OUTPUT; media_class += strlen("Output/"); } else if (spa_strstartswith(media_class, "Input/")) { direction = PW_DIRECTION_INPUT; media_class += strlen("Input/"); } else return 0; if (spa_strstartswith(media_class, "Video")) { if (direction == PW_DIRECTION_OUTPUT) { node->plugged = pw_properties_get_uint64(object->props, PW_KEY_NODE_PLUGGED, SPA_TIMESPEC_TO_NSEC(&impl->now)); } node->active = node->configured = true; } else if (spa_strstartswith(media_class, "Unknown")) { node->active = node->configured = true; } node->direction = direction; node->type = NODE_TYPE_STREAM; node->media = strdup(media_class); pw_log_debug("%p: node %d is stream %s", impl, object->id, node->media); } else { const char *media; bool virtual = false; if (spa_strstartswith(media_class, "Audio/")) { media_class += strlen("Audio/"); media = "Audio"; } else if (spa_strstartswith(media_class, "Video/")) { media_class += strlen("Video/"); media = "Video"; node->active = node->configured = true; } else return 0; if (spa_streq(media_class, "Sink") || spa_streq(media_class, "Duplex")) direction = PW_DIRECTION_INPUT; else if (spa_streq(media_class, "Source")) direction = PW_DIRECTION_OUTPUT; else if (spa_streq(media_class, "Source/Virtual")) { virtual = true; direction = PW_DIRECTION_OUTPUT; } else return 0; node->plugged = pw_properties_get_uint64(object->props, PW_KEY_NODE_PLUGGED, SPA_TIMESPEC_TO_NSEC(&impl->now)); node->priority = pw_properties_get_uint32(object->props, PW_KEY_PRIORITY_SESSION, 0); node->direction = direction; node->virtual = virtual; node->type = NODE_TYPE_DEVICE; node->media = strdup(media); pw_log_debug("%p: node %d '%s' prio:%d", impl, object->id, node->media, node->priority); } node->enabled = true; node->obj->obj.mask |= SM_NODE_CHANGE_MASK_PARAMS; sm_object_add_listener(&node->obj->obj, &node->listener, &object_events, node); return 1; } static void unpeer_node(struct node *node) { struct impl *impl = node->impl; pw_log_debug("unpeer id:%d exclusive:%d", node->id, node->exclusive); if (node->passthrough) { node->passthrough = false; node->configured = false; sm_media_session_schedule_rescan(impl->session); } } static void destroy_node(struct impl *impl, struct node *node) { pw_log_debug("destroy %d %p", node->id, node->peer); spa_list_remove(&node->link); if (node->linking) impl->linking_node_removed = true; impl->node_list_changed = true; if (node->enabled) spa_hook_remove(&node->listener); free(node->media); if (node->peer) unpeer_node(node->peer); if (node->peer && node->peer->peer == node) node->peer->peer = NULL; sm_object_remove_data((struct sm_object*)node->obj, SESSION_KEY); } static int json_object_find(const char *obj, const char *key, char *value, size_t len) { struct spa_json it[2]; const char *v; char k[128]; spa_json_init(&it[0], obj, strlen(obj)); if (spa_json_enter_object(&it[0], &it[1]) <= 0) return -EINVAL; while (spa_json_get_string(&it[1], k, sizeof(k)-1) > 0) { if (spa_streq(k, key)) { if (spa_json_get_string(&it[1], value, len) <= 0) continue; return 0; } else { if (spa_json_next(&it[1], &v) <= 0) break; } } return -ENOENT; } static bool check_node_name(struct node *node, const char *name) { const char *str; if ((str = pw_properties_get(node->obj->obj.props, PW_KEY_NODE_NAME)) != NULL && name != NULL && spa_streq(str, name)) return true; return false; } static struct node *find_node_by_id_name(struct impl *impl, uint32_t id, const char *name) { struct node *node; uint32_t name_id = name ? (uint32_t)atoi(name) : SPA_ID_INVALID; spa_list_for_each(node, &impl->node_list, link) { if (node->id == id || node->id == name_id) return node; if (check_node_name(node, name)) return node; } return NULL; } static bool can_link_check(struct impl *impl, const char *link_group, struct node *target, int hops) { const char *g, *ng; struct node *n; if (hops == 8) return false; pw_log_debug("link group %s", link_group); if (target->obj->info == NULL || target->obj->info->props == NULL) return true; g = spa_dict_lookup(target->obj->info->props, PW_KEY_NODE_LINK_GROUP); if (g == NULL) return true; if (spa_streq(g, link_group)) return false; spa_list_for_each(n, &impl->node_list, link) { if (n == target || n->direction != target->direction) continue; if (n->obj->info == NULL || n->obj->info->props == NULL) return true; ng = spa_dict_lookup(n->obj->info->props, PW_KEY_NODE_LINK_GROUP); if (ng == NULL || !spa_streq(ng, g)) continue; if (n->peer != NULL && !can_link_check(impl, link_group, n->peer, hops + 1)) return false; } return true; } static bool can_link(struct impl *impl, struct node *node, struct node *target) { const char *link_group; link_group = spa_dict_lookup(node->obj->info->props, PW_KEY_NODE_LINK_GROUP); if (link_group == NULL) return true; return can_link_check(impl, link_group, target, 0); } static const char *get_device_name(struct node *node) { if (node->type != NODE_TYPE_DEVICE || node->obj->obj.props == NULL) return NULL; return pw_properties_get(node->obj->obj.props, PW_KEY_NODE_NAME); } static uint32_t find_device_for_name(struct impl *impl, const char *name) { struct node *node; const char *str; uint32_t id = atoi(name); spa_list_for_each(node, &impl->node_list, link) { if (id == node->obj->obj.id) return id; if ((str = get_device_name(node)) == NULL) continue; if (spa_streq(str, name)) return node->obj->obj.id; } return SPA_ID_INVALID; } static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; int res; clock_gettime(CLOCK_MONOTONIC, &impl->now); if (spa_streq(object->type, PW_TYPE_INTERFACE_Node)) res = handle_node(impl, object); else res = 0; if (res < 0) { pw_log_warn("%p: can't handle global %d", impl, object->id); } else sm_media_session_schedule_rescan(impl->session); } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; pw_log_debug("%p: remove global '%d'", impl, object->id); if (spa_streq(object->type, PW_TYPE_INTERFACE_Node)) { struct node *n, *node; if ((node = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_node(impl, node); spa_list_for_each(n, &impl->node_list, link) { if (n->peer == node) n->peer = NULL; if (n->failed_peer == node) n->failed_peer = NULL; } } sm_media_session_schedule_rescan(impl->session); } static bool array_contains(const struct spa_pod *pod, uint32_t val) { uint32_t *vals, n_vals; uint32_t n; if (pod == NULL) return false; vals = spa_pod_get_array(pod, &n_vals); if (vals == NULL || n_vals == 0) return false; for (n = 0; n < n_vals; n++) if (vals[n] == val) return true; return false; } static bool have_available_route(struct node *node, struct sm_device *dev) { struct sm_param *p; const char *str; uint32_t card_profile_device; int found = 0, avail = 0; if (node->obj->info == NULL || node->obj->info->props == NULL || (str = spa_dict_lookup(node->obj->info->props, "card.profile.device")) == NULL) return 1; if (!spa_atou32(str, &card_profile_device, 0)) return 1; spa_list_for_each(p, &dev->param_list, link) { uint32_t device_id; enum spa_param_availability available; if (p->id != SPA_PARAM_Route) continue; if (spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_ParamRoute, NULL, SPA_PARAM_ROUTE_device, SPA_POD_Int(&device_id), SPA_PARAM_ROUTE_available, SPA_POD_Id(&available)) < 0) continue; /* we found the route for the device */ if (device_id != card_profile_device) continue; if (available == SPA_PARAM_AVAILABILITY_no) return 0; return 1; } /* Route is not found so no active profile. Check if there is a route that * is available */ spa_list_for_each(p, &dev->param_list, link) { struct spa_pod *devices = NULL; enum spa_param_availability available; if (p->id != SPA_PARAM_EnumRoute) continue; if (spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_ParamRoute, NULL, SPA_PARAM_ROUTE_devices, SPA_POD_OPT_Pod(&devices), SPA_PARAM_ROUTE_available, SPA_POD_Id(&available)) < 0) continue; if (!array_contains(devices, card_profile_device)) continue; found++; if (available != SPA_PARAM_AVAILABILITY_no) avail++; } if (found == 0) return 1; if (avail > 0) return 1; return 0; } struct find_data { struct impl *impl; struct node *result; struct node *node; const char *media; const char *link_group; bool capture_sink; enum pw_direction direction; bool exclusive; bool have_passthrough; bool passthrough_only; bool can_passthrough; int priority; uint64_t plugged; }; static int find_node(void *data, struct node *node) { struct find_data *find = data; struct impl *impl = find->impl; int priority = 0; uint64_t plugged = 0; struct sm_device *device = node->obj->device; bool is_default = false, can_passthrough = false; if (node->obj->info == NULL) { pw_log_debug("%p: skipping node '%d' with no node info", impl, node->id); return 0; } pw_log_debug("%p: looking at node '%d' enabled:%d state:%d peer:%p exclusive:%d", impl, node->id, node->enabled, node->obj->info->state, node->peer, node->exclusive); if (!node->enabled || node->type == NODE_TYPE_UNKNOWN) return 0; if (device && device->locked) { pw_log_debug(".. device locked"); return 0; } if (node->media && !spa_streq(node->media, find->media)) { pw_log_debug(".. incompatible media %s <-> %s", node->media, find->media); return 0; } if (find->link_group && !can_link_check(impl, find->link_group, node, 0)) { pw_log_debug(".. connecting link-group %s", find->link_group); return 0; } plugged = node->plugged; priority = node->priority; if (node->media) { if (spa_streq(node->media, "Audio")) { if (node->direction == PW_DIRECTION_INPUT) { if (find->direction == PW_DIRECTION_OUTPUT) is_default |= check_node_name(node, impl->defaults[DEFAULT_AUDIO_SINK].config); else if (find->direction == PW_DIRECTION_INPUT) is_default |= check_node_name(node, impl->defaults[DEFAULT_AUDIO_SOURCE].config); } else if (node->direction == PW_DIRECTION_OUTPUT && find->direction == PW_DIRECTION_INPUT) is_default |= check_node_name(node, impl->defaults[DEFAULT_AUDIO_SOURCE].config); } else if (spa_streq(node->media, "Video")) { if (node->direction == PW_DIRECTION_OUTPUT && find->direction == PW_DIRECTION_INPUT) is_default |= check_node_name(node, impl->defaults[DEFAULT_VIDEO_SOURCE].config); } if (is_default) priority += 10000; } if (device != NULL && !is_default && !have_available_route(node, device)) { pw_log_debug(".. no available routes"); return 0; } if ((find->capture_sink && node->direction != PW_DIRECTION_INPUT) || (!find->capture_sink && !is_default && node->direction == find->direction)) { pw_log_debug(".. same direction"); return 0; } if ((find->exclusive && node->obj->info->state == PW_NODE_STATE_RUNNING) || (node->peer && node->peer->exclusive)) { pw_log_debug("%p: node '%d' in use", impl, node->id); return 0; } if (find->node && find->have_passthrough && node->have_passthrough) can_passthrough = check_passthrough(find->node, node); if ((find->passthrough_only || node->passthrough_only) && !can_passthrough) { pw_log_debug("%p: node '%d' passthrough required", impl, node->id); return 0; } pw_log_debug("%p: found node '%d' %"PRIu64" prio:%d", impl, node->id, plugged, priority); if (find->result == NULL || priority > find->priority || (priority == find->priority && plugged > find->plugged)) { pw_log_debug("%p: new best %d %" PRIu64, impl, priority, plugged); find->result = node; find->priority = priority; find->plugged = plugged; find->can_passthrough = can_passthrough; } return 0; } static struct node *find_auto_default_node(struct impl *impl, const struct default_node *def) { struct node *node; struct find_data find; spa_zero(find); find.impl = impl; find.capture_sink = false; find.exclusive = false; if (spa_streq(def->key, DEFAULT_AUDIO_SINK_KEY)) { find.media = "Audio"; find.direction = PW_DIRECTION_OUTPUT; } else if (spa_streq(def->key, DEFAULT_AUDIO_SOURCE_KEY)) { find.media = "Audio"; find.direction = PW_DIRECTION_INPUT; } else if (spa_streq(def->key, DEFAULT_VIDEO_SOURCE_KEY)) { find.media = "Video"; find.direction = PW_DIRECTION_INPUT; } else { return NULL; } spa_list_for_each(node, &impl->node_list, link) find_node(&find, node); return find.result; } static int link_nodes(struct node *node, struct node *peer) { struct impl *impl = node->impl; struct pw_properties *props; struct node *output, *input; int res; uint32_t node_id = node->id; pw_log_debug("%p: link nodes %d %d remix:%d", impl, node->id, peer->id, !node->dont_remix); if (node->want_passthrough) { configure_passthrough(node); configure_passthrough(peer); } else { if (node->dont_remix || peer->unpositioned) configure_node(node, NULL, peer->unpositioned); else configure_node(node, &peer->format, true); } if (node->direction == PW_DIRECTION_INPUT) { output = peer; input = node; } else { output = node; input = peer; } props = pw_properties_new(NULL, NULL); pw_properties_setf(props, PW_KEY_LINK_OUTPUT_NODE, "%d", output->id); pw_properties_setf(props, PW_KEY_LINK_INPUT_NODE, "%d", input->id); pw_log_info("linking node %d to node %d", output->id, input->id); node->linking = true; res = sm_media_session_create_links(impl->session, &props->dict); pw_properties_free(props); if (impl->linking_node_removed) { impl->linking_node_removed = false; pw_log_info("linking node %d was removed", node_id); return -ENOENT; } node->linking = false; pw_log_info("linked %d to %p", res, peer); if (res > 0) { node->peer = peer; node->connect_count++; } return res; } static int unlink_nodes(struct node *node, struct node *peer) { struct impl *impl = node->impl; struct pw_properties *props; pw_log_debug("%p: unlink nodes %d %d", impl, node->id, peer->id); if (peer->peer == node) peer->peer = NULL; node->peer = NULL; if (node->direction == PW_DIRECTION_INPUT) { struct node *t = node; node = peer; peer = t; } props = pw_properties_new(NULL, NULL); pw_properties_setf(props, PW_KEY_LINK_OUTPUT_NODE, "%d", node->id); pw_properties_setf(props, PW_KEY_LINK_INPUT_NODE, "%d", peer->id); pw_log_info("unlinking node %d from peer node %d", node->id, peer->id); sm_media_session_remove_links(impl->session, &props->dict); pw_properties_free(props); return 0; } static int relink_node(struct impl *impl, struct node *n, struct node *peer) { int res; if (peer == n->failed_peer && n->failed_count > MAX_LINK_RETRY) { /* Break rescan -> failed link -> rescan loop. */ pw_log_debug("%p: tried to link '%d' on last rescan, not retrying", impl, peer->id); return -EBUSY; } if (n->failed_peer != peer) n->failed_count = 0; n->failed_peer = peer; n->failed_count++; if (!can_link(impl, n, peer)) { pw_log_debug("can't link node %d to %d: same link-group", n->id, peer->id); return -EPERM; } if (n->peer != NULL) if ((res = unlink_nodes(n, n->peer)) < 0) return res; pw_log_debug("%p: linking node %d to node %d", impl, n->id, peer->id); /* NB. if link_nodes returns error, n may have been invalidated */ if ((res = link_nodes(n, peer)) > 0) { n->failed_peer = NULL; n->failed_count = 0; } return res; } static int rescan_node(struct impl *impl, struct node *n) { struct spa_dict *props; const char *str; bool reconnect, autoconnect, can_passthrough = false; struct pw_node_info *info; struct node *peer; struct sm_object *obj; uint32_t path_id; if (!n->active) { pw_log_debug("%p: node %d is not active", impl, n->id); return 0; } if (n->type == NODE_TYPE_DEVICE) { configure_node(n, NULL, false); return 0; } if (n->obj->info == NULL || n->obj->info->props == NULL) { pw_log_debug("%p: node %d has no properties", impl, n->id); return 0; } info = n->obj->info; props = info->props; str = spa_dict_lookup(props, PW_KEY_NODE_DONT_RECONNECT); reconnect = str ? !pw_properties_parse_bool(str) : true; if ((str = spa_dict_lookup(props, PW_KEY_STREAM_DONT_REMIX)) != NULL) n->dont_remix = pw_properties_parse_bool(str); if ((str = spa_dict_lookup(props, PW_KEY_STREAM_MONITOR)) != NULL) n->monitor = pw_properties_parse_bool(str); if (n->direction == PW_DIRECTION_INPUT && (str = spa_dict_lookup(props, PW_KEY_STREAM_CAPTURE_SINK)) != NULL) n->capture_sink = pw_properties_parse_bool(str); autoconnect = false; if ((str = spa_dict_lookup(props, PW_KEY_NODE_AUTOCONNECT)) != NULL) autoconnect = pw_properties_parse_bool(str); if ((str = spa_dict_lookup(props, PW_KEY_DEVICE_API)) != NULL && spa_streq(str, "bluez5")) autoconnect = true; if (!autoconnect) { pw_log_debug("%p: node %d does not need autoconnect", impl, n->id); configure_node(n, NULL, false); return 0; } if (n->media == NULL) { pw_log_debug("%p: node %d has unknown media", impl, n->id); return 0; } pw_log_debug("%p: exclusive:%d", impl, n->exclusive); /* honor target node set by user or asked for by the client */ path_id = SPA_ID_INVALID; if (n->obj->target_node != NULL) path_id = find_device_for_name(impl, n->obj->target_node); if (!n->obj->fixed_target && (str = spa_dict_lookup(props, PW_KEY_NODE_TARGET)) != NULL) { bool has_target = ((uint32_t)atoi(str) != SPA_ID_INVALID); path_id = find_device_for_name(impl, str); if (!reconnect && has_target && path_id == SPA_ID_INVALID) { /* don't use fallbacks for non-reconnecting nodes */ peer = NULL; goto do_link; } } if (n->peer != NULL) { /* Do we need to check again where to link to? */ bool target_found = (path_id != SPA_ID_INVALID); bool peer_is_target = (target_found && n->peer->obj->obj.id == path_id); bool follows_default = (impl->streams_follow_default && n->type == NODE_TYPE_STREAM); bool recheck = !peer_is_target && (follows_default || target_found) && reconnect && !n->passthrough; if (!recheck) { pw_log_debug("%p: node %d is already linked, peer-is-target:%d " "follows-default:%d", impl, n->id, peer_is_target, follows_default); return 0; } } pw_log_info("trying to link node %d exclusive:%d reconnect:%d target:%d, peer %p", n->id, n->exclusive, reconnect, path_id, n->peer); if (path_id != SPA_ID_INVALID) { pw_log_debug("%p: target:%d", impl, path_id); if (!reconnect) n->obj->target_node = NULL; if ((obj = sm_media_session_find_object(impl->session, path_id)) == NULL) { pw_log_warn("node %d target:%d not found, find fallback:%d", n->id, path_id, reconnect); path_id = SPA_ID_INVALID; goto fallback; } path_id = SPA_ID_INVALID; if (!spa_streq(obj->type, PW_TYPE_INTERFACE_Node)) goto fallback; peer = sm_object_get_data(obj, SESSION_KEY); pw_log_debug("%p: found target:%d type:%s %d:%d", impl, peer->id, obj->type, n->passthrough_only, peer->have_passthrough); can_passthrough = check_passthrough(n, peer); if (n->passthrough_only && !can_passthrough) { pw_log_info("%p: peer has no passthrough", impl); goto fallback; } goto do_link; } fallback: if (path_id == SPA_ID_INVALID && (reconnect || n->connect_count == 0)) { /* find fallback */ struct find_data find; spa_zero(find); find.impl = impl; find.node = n; find.media = n->media; find.capture_sink = n->capture_sink; find.direction = n->direction; find.exclusive = n->exclusive; find.have_passthrough = n->have_passthrough; find.passthrough_only = n->passthrough_only; find.link_group = n->peer == NULL ? spa_dict_lookup(props, PW_KEY_NODE_LINK_GROUP) : NULL; spa_list_for_each(peer, &impl->node_list, link) find_node(&find, peer); peer = find.result; if (peer) can_passthrough = find.can_passthrough; if (n->passthrough_only && !can_passthrough) peer = NULL; } else { peer = NULL; } do_link: if (peer == NULL) { if (!reconnect) { pw_log_info("don-reconnect target node destroyed: destroy %d", n->id); sm_media_session_destroy_object(impl->session, n->id); } else if (reconnect && n->connect_count > 0) { /* Don't error the stream on reconnects */ pw_log_info("%p: no node found for %d, waiting reconnect", impl, n->id); if (n->peer != NULL) unlink_nodes(n, n->peer); return 0; } else { pw_log_warn("%p: no node found for %d, stream error", impl, n->id); } obj = sm_media_session_find_object(impl->session, n->client_id); pw_log_debug("%p: client_id:%d object:%p type:%s", impl, n->client_id, obj, obj ? obj->type : "None"); if (obj && spa_streq(obj->type, PW_TYPE_INTERFACE_Client)) { pw_client_error((struct pw_client*)obj->proxy, n->id, -ENOENT, "no node available"); } return -ENOENT; } else if (peer == n->peer) { pw_log_debug("%p: node %d already linked to %d (not changing)", impl, n->id, peer->id); return 0; } else { n->want_passthrough = can_passthrough; } if ((n->exclusive || n->want_passthrough) && peer->obj->info->state == PW_NODE_STATE_RUNNING) { pw_log_warn("node %d busy, can't get exclusive/passthrough access", peer->id); return -EBUSY; } return relink_node(impl, n, peer); } static void session_info(void *data, const struct pw_core_info *info) { struct impl *impl = data; if (info && (info->change_mask & PW_CORE_CHANGE_MASK_PROPS)) { const char *str; if ((str = spa_dict_lookup(info->props, "default.clock.rate")) != NULL) impl->sample_rate = atoi(str); pw_log_debug("%p: props changed sample_rate:%d", impl, impl->sample_rate); } } static void refresh_auto_default_nodes(struct impl *impl) { struct default_node *def; if (impl->session->metadata == NULL) return; pw_log_debug("%p: refresh", impl); /* Auto set default nodes */ for (def = impl->defaults; def->key != NULL; ++def) { struct node *node; node = find_auto_default_node(impl, def); if (node == NULL && def->value != NULL) { def->value = NULL; pw_metadata_set_property(impl->session->metadata, PW_ID_CORE, def->key, NULL, NULL); } else if (node != NULL) { const char *name = pw_properties_get(node->obj->obj.props, PW_KEY_NODE_NAME); char buf[1024]; if (name == NULL || spa_streq(name, def->value)) continue; free(def->value); def->value = strdup(name); snprintf(buf, sizeof(buf), "{ \"name\": \"%s\" }", name); pw_metadata_set_property(impl->session->metadata, PW_ID_CORE, def->key, "Spa:String:JSON", buf); } } } static void session_rescan(void *data, int seq) { struct impl *impl = data; struct node *node; pw_log_debug("%p: rescan", impl); again: impl->node_list_changed = false; spa_list_for_each(node, &impl->node_list, link) { rescan_node(impl, node); if (impl->node_list_changed) goto again; } refresh_auto_default_nodes(impl); } static void session_destroy(void *data) { struct impl *impl = data; struct default_node *def; for (def = impl->defaults; def->key != NULL; ++def) { free(def->config); free(def->value); } spa_hook_remove(&impl->listener); if (impl->session->metadata) spa_hook_remove(&impl->meta_listener); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .info = session_info, .create = session_create, .remove = session_remove, .rescan = session_rescan, .destroy = session_destroy, }; static int metadata_property(void *object, uint32_t subject, const char *key, const char *type, const char *value) { struct impl *impl = object; if (subject == PW_ID_CORE) { struct default_node *def; bool changed = false; char *val = NULL; char name[1024]; if (key != NULL && value != NULL) { pw_log_info("meta %s: %s", key, value); if (json_object_find(value, "name", name, sizeof(name)) < 0) return 0; pw_log_info("meta name: %s", name); val = name; } for (def = impl->defaults; def->key != NULL; ++def) { if (key == NULL || spa_streq(key, def->key_config)) { if (!spa_streq(def->config, val)) changed = true; free(def->config); def->config = val ? strdup(val) : NULL; } if (key == NULL || spa_streq(key, def->key)) { bool eff_changed = !spa_streq(def->value, val); free(def->value); def->value = val ? strdup(val) : NULL; /* The effective value was changed. In case it was changed by * someone else than us, reset the value to avoid confusion. */ if (eff_changed) refresh_auto_default_nodes(impl); } } if (changed) sm_media_session_schedule_rescan(impl->session); } else if (key == NULL || spa_streq(key, "target.node")) { struct node *src_node; src_node = find_node_by_id_name(impl, subject, NULL); if (!src_node) return 0; /* Set target and schedule rescan */ if (key == NULL || value == NULL) { free(src_node->obj->target_node); src_node->obj->target_node = NULL; src_node->obj->fixed_target = false; } else { const char *str; struct node *dst_node; dst_node = find_node_by_id_name(impl, SPA_ID_INVALID, value); if (dst_node) { str = get_device_name(dst_node); if (!str) return 0; } else if ((uint32_t)atoi(value) == SPA_ID_INVALID) { str = NULL; } else { return 0; } free(src_node->obj->target_node); src_node->obj->target_node = str ? strdup(str) : NULL; src_node->obj->fixed_target = true; } sm_media_session_schedule_rescan(impl->session); } return 0; } static const struct pw_metadata_events metadata_events = { PW_VERSION_METADATA_EVENTS, .property = metadata_property, }; int sm_policy_node_start(struct sm_media_session *session) { struct impl *impl; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; impl->context = session->context; impl->sample_rate = 48000; impl->defaults[DEFAULT_AUDIO_SINK] = (struct default_node){ DEFAULT_AUDIO_SINK_KEY, DEFAULT_CONFIG_AUDIO_SINK_KEY, NULL, NULL }; impl->defaults[DEFAULT_AUDIO_SOURCE] = (struct default_node){ DEFAULT_AUDIO_SOURCE_KEY, DEFAULT_CONFIG_AUDIO_SOURCE_KEY, NULL, NULL }; impl->defaults[DEFAULT_VIDEO_SOURCE] = (struct default_node){ DEFAULT_VIDEO_SOURCE_KEY, DEFAULT_CONFIG_VIDEO_SOURCE_KEY, NULL, NULL }; impl->defaults[3] = (struct default_node){ NULL, NULL, NULL, NULL }; impl->streams_follow_default = pw_properties_get_bool(session->props, NAME ".streams-follow-default", false); impl->alsa_no_dsp = pw_properties_get_bool(session->props, NAME ".alsa-no-dsp", false); spa_list_init(&impl->node_list); sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); if (session->metadata) { pw_metadata_add_listener(session->metadata, &impl->meta_listener, &metadata_events, impl); } return 0; } 07070100000072000081A40000000000000000000000016178A88C00003328000000000000000000000000000000000000002200000000media-session-0.4.1/src/reserve.c/* DBus device reservation API * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #ifndef NAME #define NAME "reserve" #endif #include "reserve.h" #include <spa/utils/string.h> #include <pipewire/log.h> #define SERVICE_PREFIX "org.freedesktop.ReserveDevice1." #define OBJECT_PREFIX "/org/freedesktop/ReserveDevice1/" static const char introspection[] = DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE "<node>" " <!-- If you are looking for documentation make sure to check out\n" " http://git.0pointer.de/?p=reserve.git;a=blob;f=reserve.txt -->\n" " <interface name=\"org.freedesktop.ReserveDevice1\">" " <method name=\"RequestRelease\">" " <arg name=\"priority\" type=\"i\" direction=\"in\"/>" " <arg name=\"result\" type=\"b\" direction=\"out\"/>" " </method>" " <property name=\"Priority\" type=\"i\" access=\"read\"/>" " <property name=\"ApplicationName\" type=\"s\" access=\"read\"/>" " <property name=\"ApplicationDeviceName\" type=\"s\" access=\"read\"/>" " </interface>" " <interface name=\"org.freedesktop.DBus.Properties\">" " <method name=\"Get\">" " <arg name=\"interface\" direction=\"in\" type=\"s\"/>" " <arg name=\"property\" direction=\"in\" type=\"s\"/>" " <arg name=\"value\" direction=\"out\" type=\"v\"/>" " </method>" " </interface>" " <interface name=\"org.freedesktop.DBus.Introspectable\">" " <method name=\"Introspect\">" " <arg name=\"data\" type=\"s\" direction=\"out\"/>" " </method>" " </interface>" "</node>"; struct rd_device { DBusConnection *connection; int32_t priority; char *service_name; char *object_path; char *application_name; char *application_device_name; const struct rd_device_callbacks *callbacks; void *data; DBusMessage *reply; unsigned int filtering:1; unsigned int registered:1; unsigned int acquiring:1; unsigned int owning:1; }; static dbus_bool_t add_variant(DBusMessage *m, int type, const void *data) { DBusMessageIter iter, sub; char t[2]; t[0] = (char) type; t[1] = 0; dbus_message_iter_init_append(m, &iter); if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_VARIANT, t, &sub)) return false; if (!dbus_message_iter_append_basic(&sub, type, data)) return false; if (!dbus_message_iter_close_container(&iter, &sub)) return false; return true; } static DBusHandlerResult object_handler(DBusConnection *c, DBusMessage *m, void *userdata) { struct rd_device *d = userdata; DBusError error; DBusMessage *reply = NULL; dbus_error_init(&error); if (dbus_message_is_method_call(m, "org.freedesktop.ReserveDevice1", "RequestRelease")) { int32_t priority; if (!dbus_message_get_args(m, &error, DBUS_TYPE_INT32, &priority, DBUS_TYPE_INVALID)) goto invalid; pw_log_debug("%p: request release priority:%d", d, priority); if (!(reply = dbus_message_new_method_return(m))) goto oom; if (d->reply) rd_device_complete_release(d, false); d->reply = reply; if (priority > d->priority && d->callbacks->release) d->callbacks->release(d->data, d, 0); else rd_device_complete_release(d, false); return DBUS_HANDLER_RESULT_HANDLED; } else if (dbus_message_is_method_call( m, "org.freedesktop.DBus.Properties", "Get")) { const char *interface, *property; if (!dbus_message_get_args( m, &error, DBUS_TYPE_STRING, &interface, DBUS_TYPE_STRING, &property, DBUS_TYPE_INVALID)) goto invalid; if (spa_streq(interface, "org.freedesktop.ReserveDevice1")) { const char *empty = ""; if (spa_streq(property, "ApplicationName") && d->application_name) { if (!(reply = dbus_message_new_method_return(m))) goto oom; if (!add_variant(reply, DBUS_TYPE_STRING, d->application_name ? (const char**) &d->application_name : &empty)) goto oom; } else if (spa_streq(property, "ApplicationDeviceName")) { if (!(reply = dbus_message_new_method_return(m))) goto oom; if (!add_variant(reply, DBUS_TYPE_STRING, d->application_device_name ? (const char**) &d->application_device_name : &empty)) goto oom; } else if (spa_streq(property, "Priority")) { if (!(reply = dbus_message_new_method_return(m))) goto oom; if (!add_variant(reply, DBUS_TYPE_INT32, &d->priority)) goto oom; } else { if (!(reply = dbus_message_new_error_printf(m, DBUS_ERROR_UNKNOWN_METHOD, "Unknown property %s", property))) goto oom; } if (!dbus_connection_send(c, reply, NULL)) goto oom; dbus_message_unref(reply); return DBUS_HANDLER_RESULT_HANDLED; } } else if (dbus_message_is_method_call( m, "org.freedesktop.DBus.Introspectable", "Introspect")) { const char *i = introspection; if (!(reply = dbus_message_new_method_return(m))) goto oom; if (!dbus_message_append_args(reply, DBUS_TYPE_STRING, &i, DBUS_TYPE_INVALID)) goto oom; if (!dbus_connection_send(c, reply, NULL)) goto oom; dbus_message_unref(reply); return DBUS_HANDLER_RESULT_HANDLED; } return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; invalid: if (!(reply = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid arguments"))) goto oom; if (!dbus_connection_send(c, reply, NULL)) goto oom; dbus_message_unref(reply); dbus_error_free(&error); return DBUS_HANDLER_RESULT_HANDLED; oom: if (reply) dbus_message_unref(reply); dbus_error_free(&error); return DBUS_HANDLER_RESULT_NEED_MEMORY; } static const struct DBusObjectPathVTable vtable ={ .message_function = object_handler }; static DBusHandlerResult filter_handler(DBusConnection *c, DBusMessage *m, void *userdata) { struct rd_device *d = userdata; DBusError error; const char *name; dbus_error_init(&error); if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameAcquired")) { if (!dbus_message_get_args( m, &error, DBUS_TYPE_STRING, &name, DBUS_TYPE_INVALID)) goto invalid; if (!spa_streq(name, d->service_name)) goto invalid; pw_log_debug("%p: acquired %s, %s", d, name, d->service_name); d->owning = true; if (!d->registered) { if (!(dbus_connection_register_object_path(d->connection, d->object_path, &vtable, d))) goto invalid; if (!spa_streq(name, d->service_name)) goto invalid; d->registered = true; if (d->callbacks->acquired) d->callbacks->acquired(d->data, d); } } else if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameLost")) { if (!dbus_message_get_args( m, &error, DBUS_TYPE_STRING, &name, DBUS_TYPE_INVALID)) goto invalid; if (!spa_streq(name, d->service_name)) goto invalid; pw_log_debug("%p: lost %s", d, name); d->owning = false; if (d->registered) { dbus_connection_unregister_object_path(d->connection, d->object_path); d->registered = false; } } if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) { const char *old, *new; if (!dbus_message_get_args( m, &error, DBUS_TYPE_STRING, &name, DBUS_TYPE_STRING, &old, DBUS_TYPE_STRING, &new, DBUS_TYPE_INVALID)) goto invalid; if (!spa_streq(name, d->service_name) || d->owning) goto invalid; pw_log_debug("%p: changed %s: %s -> %s", d, name, old, new); if (old == NULL || *old == 0) { if (d->callbacks->busy && !d->acquiring) d->callbacks->busy(d->data, d, name, 0); } else { if (d->callbacks->available) d->callbacks->available(d->data, d, name); } } invalid: dbus_error_free(&error); return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } struct rd_device * rd_device_new(DBusConnection *connection, const char *device_name, const char *application_name, int32_t priority, const struct rd_device_callbacks *callbacks, void *data) { struct rd_device *d; int res; d = calloc(1, sizeof(struct rd_device)); if (d == NULL) return NULL; d->connection = connection; d->priority = priority; d->callbacks = callbacks; d->data = data; d->application_name = strdup(application_name); d->object_path = spa_aprintf(OBJECT_PREFIX "%s", device_name); if (d->object_path == NULL) { res = -errno; goto error_free; } d->service_name = spa_aprintf(SERVICE_PREFIX "%s", device_name); if (d->service_name == NULL) { res = -errno; goto error_free; } if (!dbus_connection_add_filter(d->connection, filter_handler, d, NULL)) { res = -ENOMEM; goto error_free; } dbus_bus_add_match(d->connection, "type='signal',sender='org.freedesktop.DBus'," "interface='org.freedesktop.DBus',member='NameLost'", NULL); dbus_bus_add_match(d->connection, "type='signal',sender='org.freedesktop.DBus'," "interface='org.freedesktop.DBus',member='NameAcquired'", NULL); dbus_bus_add_match(d->connection, "type='signal',sender='org.freedesktop.DBus'," "interface='org.freedesktop.DBus',member='NameOwnerChanged'", NULL); dbus_connection_ref(d->connection); pw_log_debug("%p: new device %s", d, device_name); return d; error_free: free(d->service_name); free(d->object_path); free(d); errno = -res; return NULL; } int rd_device_acquire(struct rd_device *d) { int res; DBusError error; dbus_error_init(&error); pw_log_debug("%p: reserve %s", d, d->service_name); d->acquiring = true; if ((res = dbus_bus_request_name(d->connection, d->service_name, (d->priority < INT32_MAX ? DBUS_NAME_FLAG_ALLOW_REPLACEMENT : 0), &error)) < 0) { pw_log_warn("%p: reserve failed: %s", d, error.message); dbus_error_free(&error); return -EIO; } pw_log_debug("%p: reserve result: %d", d, res); if (res == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER || res == DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER) return 0; if (res == DBUS_REQUEST_NAME_REPLY_EXISTS || res == DBUS_REQUEST_NAME_REPLY_IN_QUEUE) return -EBUSY; return -EIO; } int rd_device_request_release(struct rd_device *d) { DBusMessage *m = NULL; if (d->priority <= INT32_MIN) return -EBUSY; if ((m = dbus_message_new_method_call(d->service_name, d->object_path, "org.freedesktop.ReserveDevice1", "RequestRelease")) == NULL) { return -ENOMEM; } if (!dbus_message_append_args(m, DBUS_TYPE_INT32, &d->priority, DBUS_TYPE_INVALID)) { dbus_message_unref(m); return -ENOMEM; } if (!dbus_connection_send(d->connection, m, NULL)) { return -EIO; } return 0; } int rd_device_complete_release(struct rd_device *d, int res) { dbus_bool_t ret = res != 0; if (d->reply == NULL) return -EINVAL; pw_log_debug("%p: complete release %d", d, res); if (!dbus_message_append_args(d->reply, DBUS_TYPE_BOOLEAN, &ret, DBUS_TYPE_INVALID)) { res = -ENOMEM; goto exit; } if (!dbus_connection_send(d->connection, d->reply, NULL)) { res = -EIO; goto exit; } res = 0; exit: dbus_message_unref(d->reply); d->reply = NULL; return res; } void rd_device_release(struct rd_device *d) { pw_log_debug("%p: release %d", d, d->owning); if (d->owning) { DBusError error; dbus_error_init(&error); dbus_bus_release_name(d->connection, d->service_name, &error); dbus_error_free(&error); } d->acquiring = false; } void rd_device_destroy(struct rd_device *d) { dbus_connection_remove_filter(d->connection, filter_handler, d); if (d->registered) dbus_connection_unregister_object_path(d->connection, d->object_path); rd_device_release(d); free(d->service_name); free(d->object_path); free(d->application_name); free(d->application_device_name); if (d->reply) dbus_message_unref(d->reply); dbus_connection_unref(d->connection); free(d); } int rd_device_set_application_device_name(struct rd_device *d, const char *name) { char *t; if (!d) return -EINVAL; if (!(t = strdup(name))) return -ENOMEM; free(d->application_device_name); d->application_device_name = t; return 0; } 07070100000073000081A40000000000000000000000016178A88C00000BCB000000000000000000000000000000000000002200000000media-session-0.4.1/src/reserve.h/* DBus device reservation API * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #ifndef DEVICE_RESERVE_H #define DEVICE_RESERVE_H #include <dbus/dbus.h> #include <inttypes.h> #ifdef __cplusplus extern "C" { #endif struct rd_device; struct rd_device_callbacks { /** the device is acquired by us */ void (*acquired) (void *data, struct rd_device *d); /** request a release of the device */ void (*release) (void *data, struct rd_device *d, int forced); /** the device is busy by someone else */ void (*busy) (void *data, struct rd_device *d, const char *name, int32_t priority); /** the device is made available by someone else */ void (*available) (void *data, struct rd_device *d, const char *name); }; /* create a new device and start watching */ struct rd_device * rd_device_new(DBusConnection *connection, /**< Bus to watch */ const char *device_name, /**< The device to lock, e.g. "Audio0" */ const char *application_name, /**< A human readable name of the application, * e.g. "PipeWire Server" */ int32_t priority, /**< The priority for this application. * If unsure use 0 */ const struct rd_device_callbacks *callbacks, /**< Called when device name is acquired/released */ void *data); /** try to acquire the device */ int rd_device_acquire(struct rd_device *d); /** request the owner to release the device */ int rd_device_request_release(struct rd_device *d); /** complete the release of the device */ int rd_device_complete_release(struct rd_device *d, int res); /** release a device */ void rd_device_release(struct rd_device *d); /** destroy a device */ void rd_device_destroy(struct rd_device *d); /* Set the application device name for an rd_device object. Returns 0 * on success, a negative errno style return value on error. */ int rd_device_set_application_device_name(struct rd_device *d, const char *name); #ifdef __cplusplus } #endif #endif 07070100000074000081A40000000000000000000000016178A88C00003CAB000000000000000000000000000000000000002900000000media-session-0.4.1/src/restore-stream.c/* PipeWire * * Copyright © 2020 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include <fcntl.h> #include <unistd.h> #include "config.h" #include <spa/utils/hook.h> #include <spa/utils/result.h> #include <spa/utils/json.h> #include <spa/utils/string.h> #include <spa/pod/parser.h> #include <spa/pod/builder.h> #include <spa/debug/pod.h> #include "pipewire/pipewire.h" #include "pipewire/extensions/metadata.h" #include "media-session.h" /** \page page_media_session_module_restore_stream Media Session Module: Restore Stream * * The Restore Stream modules monitors \ref pw_node * of media class `"Stream/..."` and `"Audio/..."` and saves the \ref * SPA_PROP_volume, \ref SPA_PROP_mute, and \ref SPA_PROP_channelMap * parameters to the `route-settings` state file and the `route-settings` * metadata objects. * * When a stream re-appears and matches a saved state, the parameters are * restored to their respective values. Additionally, the target node is saved * so the stream can be re-associated with that node, if possible. * * To match a stream with a previously saved state, this module uses one of * \ref PW_KEY_MEDIA_ROLE, \ref PW_KEY_APP_ID, * \ref PW_KEY_APP_NAME, \ref PW_KEY_MEDIA_NAME, and \ref PW_KEY_NODE_NAME, * whichever applies. * * The state file is `$XDG_STATE_HOME/pipewire/media-session.d/restore-stream`. * * The `route-settings` metadata object is owned by this module. */ #define NAME "restore-stream" #define SESSION_KEY "restore-stream" #define PREFIX "restore.stream." PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic #define SAVE_INTERVAL 1 struct impl { struct timespec now; struct sm_media_session *session; struct spa_hook listener; struct pw_context *context; struct spa_source *idle_timeout; struct pw_metadata *metadata; struct spa_hook metadata_listener; struct pw_properties *props; unsigned int sync:1; }; struct stream { struct sm_node *obj; uint32_t id; struct impl *impl; char *media_class; char *key; unsigned int restored:1; struct spa_hook listener; }; static void remove_idle_timeout(struct impl *impl) { struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); int res; if (impl->idle_timeout) { if ((res = sm_media_session_save_state(impl->session, SESSION_KEY, impl->props)) < 0) pw_log_error("can't save "SESSION_KEY" state: %s", spa_strerror(res)); pw_loop_destroy_source(main_loop, impl->idle_timeout); impl->idle_timeout = NULL; } } static void idle_timeout(void *data, uint64_t expirations) { struct impl *impl = data; pw_log_debug("%p: idle timeout", impl); remove_idle_timeout(impl); } static void add_idle_timeout(struct impl *impl) { struct timespec value; struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); if (impl->idle_timeout == NULL) impl->idle_timeout = pw_loop_add_timer(main_loop, idle_timeout, impl); value.tv_sec = SAVE_INTERVAL; value.tv_nsec = 0; pw_loop_update_timer(main_loop, impl->idle_timeout, &value, NULL, false); } static void session_destroy(void *data) { struct impl *impl = data; remove_idle_timeout(impl); spa_hook_remove(&impl->listener); pw_properties_free(impl->props); free(impl); } static uint32_t channel_from_name(const char *name) { int i; for (i = 0; spa_type_audio_channel[i].name; i++) { if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) return spa_type_audio_channel[i].type; } return SPA_AUDIO_CHANNEL_UNKNOWN; } static const char *channel_to_name(uint32_t channel) { int i; for (i = 0; spa_type_audio_channel[i].name; i++) { if (spa_type_audio_channel[i].type == channel) return spa_debug_type_short_name(spa_type_audio_channel[i].name); } return "UNK"; } static char *serialize_props(struct stream *str, const struct spa_pod *param) { struct spa_pod_prop *prop; struct spa_pod_object *obj = (struct spa_pod_object *) param; float val = 0.0f; bool b = false, comma = false; char *ptr; size_t size; FILE *f; f = open_memstream(&ptr, &size); fprintf(f, "{ "); SPA_POD_OBJECT_FOREACH(obj, prop) { switch (prop->key) { case SPA_PROP_volume: if (spa_pod_get_float(&prop->value, &val) < 0) continue; fprintf(f, "%s\"volume\": %f", (comma ? ", " : ""), val); break; case SPA_PROP_mute: if (spa_pod_get_bool(&prop->value, &b) < 0) continue; fprintf(f, "%s\"mute\": %s", (comma ? ", " : ""), b ? "true" : "false"); break; case SPA_PROP_channelVolumes: { uint32_t i, n_vals; float vals[SPA_AUDIO_MAX_CHANNELS]; n_vals = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, vals, SPA_AUDIO_MAX_CHANNELS); if (n_vals == 0) continue; fprintf(f, "%s\"volumes\": [", (comma ? ", " : "")); for (i = 0; i < n_vals; i++) fprintf(f, "%s%f", (i == 0 ? " ":", "), vals[i]); fprintf(f, " ]"); break; } case SPA_PROP_channelMap: { uint32_t i, n_ch; uint32_t map[SPA_AUDIO_MAX_CHANNELS]; n_ch = spa_pod_copy_array(&prop->value, SPA_TYPE_Id, map, SPA_AUDIO_MAX_CHANNELS); if (n_ch == 0) continue; fprintf(f, "%s\"channels\": [", (comma ? ", " : "")); for (i = 0; i < n_ch; i++) fprintf(f, "%s\"%s\"", (i == 0 ? " ":", "), channel_to_name(map[i])); fprintf(f, " ]"); break; } default: continue; } comma = true; } if (str->obj->target_node != NULL) fprintf(f, "%s\"target-node\": \"%s\"", (comma ? ", " : ""), str->obj->target_node); fprintf(f, " }"); fclose(f); if (strlen(ptr) < 5) { free(ptr); ptr = NULL; } return ptr; } static void sync_metadata(struct impl *impl) { const struct spa_dict_item *it; impl->sync = true; spa_dict_for_each(it, &impl->props->dict) pw_metadata_set_property(impl->metadata, PW_ID_CORE, it->key, "Spa:String:JSON", it->value); impl->sync = false; } static int metadata_property(void *object, uint32_t subject, const char *key, const char *type, const char *value) { struct impl *impl = object; int changed = 0; if (impl->sync) return 0; if (subject == PW_ID_CORE) { if (key == NULL) { pw_properties_clear(impl->props); changed = 1; } else if (spa_strstartswith(key, PREFIX)) { changed += pw_properties_set(impl->props, key, value); } } if (changed > 0) add_idle_timeout(impl); return 0; } static const struct pw_metadata_events metadata_events = { PW_VERSION_METADATA_EVENTS, .property = metadata_property, }; static int handle_props(struct stream *str, struct sm_param *p) { struct impl *impl = str->impl; const char *key; int changed = 0; if ((key = str->key) == NULL) return -EBUSY; if (p->param) { char *val = serialize_props(str, p->param); if (val) { pw_log_info("stream %d: save props %s %s", str->id, key, val); changed += pw_properties_set(impl->props, key, val); free(val); add_idle_timeout(impl); } } if (changed) sync_metadata(impl); return 0; } static int restore_stream(struct stream *str) { struct impl *impl = str->impl; struct spa_json it[3]; const char *val, *value; char buf[1024], key[128]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); struct spa_pod_frame f[2]; struct spa_pod *param; if (str->key == NULL) return -EBUSY; val = pw_properties_get(impl->props, str->key); if (val == NULL) return -ENOENT; pw_log_info("stream %d: restoring '%s' to %s", str->id, str->key, val); spa_json_init(&it[0], val, strlen(val)); if (spa_json_enter_object(&it[0], &it[1]) <= 0) return -EINVAL; spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props); while (spa_json_get_string(&it[1], key, sizeof(key)-1) > 0) { if (spa_streq(key, "volume")) { float vol; if (spa_json_get_float(&it[1], &vol) <= 0) continue; spa_pod_builder_prop(&b, SPA_PROP_volume, 0); spa_pod_builder_float(&b, vol); } else if (spa_streq(key, "mute")) { bool mute; if (spa_json_get_bool(&it[1], &mute) <= 0) continue; spa_pod_builder_prop(&b, SPA_PROP_mute, 0); spa_pod_builder_bool(&b, mute); } else if (spa_streq(key, "volumes")) { uint32_t n_vols; float vols[SPA_AUDIO_MAX_CHANNELS]; if (spa_json_enter_array(&it[1], &it[2]) <= 0) continue; for (n_vols = 0; n_vols < SPA_AUDIO_MAX_CHANNELS; n_vols++) { if (spa_json_get_float(&it[2], &vols[n_vols]) <= 0) break; } if (n_vols == 0) continue; spa_pod_builder_prop(&b, SPA_PROP_channelVolumes, 0); spa_pod_builder_array(&b, sizeof(float), SPA_TYPE_Float, n_vols, vols); } else if (spa_streq(key, "channels")) { uint32_t n_ch; uint32_t map[SPA_AUDIO_MAX_CHANNELS]; if (spa_json_enter_array(&it[1], &it[2]) <= 0) continue; for (n_ch = 0; n_ch < SPA_AUDIO_MAX_CHANNELS; n_ch++) { char chname[16]; if (spa_json_get_string(&it[2], chname, sizeof(chname)) <= 0) break; map[n_ch] = channel_from_name(chname); } if (n_ch == 0) continue; spa_pod_builder_prop(&b, SPA_PROP_channelMap, 0); spa_pod_builder_array(&b, sizeof(uint32_t), SPA_TYPE_Id, n_ch, map); } else if (spa_streq(key, "target-node")) { char name[1024]; if (spa_json_get_string(&it[1], name, sizeof(name)) <= 0) continue; pw_log_info("stream %d: target '%s'", str->obj->obj.id, name); if (!str->obj->fixed_target) { free(str->obj->target_node); str->obj->target_node = strdup(name); } } else { if (spa_json_next(&it[1], &value) <= 0) break; } } param = spa_pod_builder_pop(&b, &f[0]); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, param); pw_node_set_param((struct pw_node*)str->obj->obj.proxy, SPA_PARAM_Props, 0, param); sm_media_session_schedule_rescan(str->impl->session); return 0; } static int save_stream(struct stream *str) { struct sm_param *p; spa_list_for_each(p, &str->obj->param_list, link) { if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, p->param); switch (p->id) { case SPA_PARAM_Props: handle_props(str, p); break; default: break; } } return 0; } static void update_stream(struct stream *str) { static const char * const keys[] = { PW_KEY_MEDIA_ROLE, PW_KEY_APP_ID, PW_KEY_APP_NAME, PW_KEY_MEDIA_NAME, PW_KEY_NODE_NAME, }; struct impl *impl = str->impl; uint32_t i; const char *p; char *key; struct sm_object *obj = &str->obj->obj; key = NULL; for (i = 0; i < SPA_N_ELEMENTS(keys); i++) { if ((p = pw_properties_get(obj->props, keys[i]))) { key = spa_aprintf(PREFIX"%s.%s:%s", str->media_class, keys[i], p); break; } } if (key == NULL) return; pw_log_debug("%p: stream %p key '%s'", impl, str, key); free(str->key); str->key = key; if (!str->restored) { restore_stream(str); str->restored = true; } else { save_stream(str); } } static void object_update(void *data) { struct stream *str = data; struct impl *impl = str->impl; pw_log_info("%p: stream %p %08x/%08x", impl, str, str->obj->obj.changed, str->obj->obj.avail); if (str->obj->obj.changed & (SM_NODE_CHANGE_MASK_INFO | SM_NODE_CHANGE_MASK_PARAMS)) update_stream(str); } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; struct stream *str; const char *media_class, *routes; if (!spa_streq(object->type, PW_TYPE_INTERFACE_Node) || object->props == NULL || (media_class = pw_properties_get(object->props, PW_KEY_MEDIA_CLASS)) == NULL) return; if (spa_strstartswith(media_class, "Stream/")) { media_class += strlen("Stream/"); pw_log_debug("%p: add stream '%d' %s", impl, object->id, media_class); } else if (spa_strstartswith(media_class, "Audio/") && ((routes = pw_properties_get(object->props, "device.routes")) == NULL || atoi(routes) == 0)) { pw_log_debug("%p: add node '%d' %s", impl, object->id, media_class); } else { return; } str = sm_object_add_data(object, SESSION_KEY, sizeof(struct stream)); str->obj = (struct sm_node*)object; str->id = object->id; str->impl = impl; str->media_class = strdup(media_class); str->obj->obj.mask |= SM_OBJECT_CHANGE_MASK_PROPERTIES | SM_NODE_CHANGE_MASK_PARAMS; sm_object_add_listener(&str->obj->obj, &str->listener, &object_events, str); } static void destroy_stream(struct impl *impl, struct stream *str) { remove_idle_timeout(impl); spa_hook_remove(&str->listener); free(str->media_class); free(str->key); sm_object_remove_data((struct sm_object*)str->obj, SESSION_KEY); } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; struct stream *str; if (!spa_streq(object->type, PW_TYPE_INTERFACE_Node)) return; pw_log_debug("%p: remove node '%d'", impl, object->id); if ((str = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_stream(impl, str); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .destroy = session_destroy, }; int sm_restore_stream_start(struct sm_media_session *session) { struct impl *impl; int res; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; impl->context = session->context; impl->props = pw_properties_new(NULL, NULL); if (impl->props == NULL) goto exit_errno; impl->metadata = sm_media_session_export_metadata(session, "route-settings"); if (impl->metadata == NULL) goto exit_errno; pw_metadata_add_listener(impl->metadata, &impl->metadata_listener, &metadata_events, impl); if ((res = sm_media_session_load_state(impl->session, SESSION_KEY, impl->props)) < 0) pw_log_info("can't load "SESSION_KEY" state: %s", spa_strerror(res)); sync_metadata(impl); sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); return 0; exit_errno: res = -errno; pw_properties_free(impl->props); free(impl); return res; } 07070100000075000081A40000000000000000000000016178A88C000014BA000000000000000000000000000000000000002A00000000media-session-0.4.1/src/session-manager.c/* PipeWire * * Copyright © 2020 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/param/audio/format-utils.h> #include <spa/param/props.h> #include <spa/debug/pod.h> #include "pipewire/pipewire.h" #include "pipewire/extensions/session-manager.h" #include "media-session.h" /** \page page_media_session_module_session_manager Media Session Module: Session Manager */ #define NAME "session-manager" #define SESSION_KEY "session-manager" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic int sm_stream_endpoint_start(struct sm_media_session *sess); int sm_v4l2_endpoint_start(struct sm_media_session *sess); int sm_bluez5_endpoint_start(struct sm_media_session *sess); int sm_alsa_endpoint_start(struct sm_media_session *sess); int sm_policy_ep_start(struct sm_media_session *sess); struct impl { struct timespec now; struct sm_media_session *session; struct spa_hook listener; struct pw_context *context; struct spa_hook proxy_client_session_listener; struct spa_hook client_session_listener; }; /** * Session implementation */ static int client_session_set_param(void *object, uint32_t id, uint32_t flags, const struct spa_pod *param) { struct impl *impl = object; pw_proxy_error((struct pw_proxy*)impl->session->client_session, -ENOTSUP, "Session:SetParam not supported"); return -ENOTSUP; } static int client_session_link_set_param(void *object, uint32_t link_id, uint32_t id, uint32_t flags, const struct spa_pod *param) { struct impl *impl = object; pw_proxy_error((struct pw_proxy*)impl->session->client_session, -ENOTSUP, "Session:LinkSetParam not supported"); return -ENOTSUP; } static int client_session_link_request_state(void *object, uint32_t link_id, uint32_t state) { return -ENOTSUP; } static const struct pw_client_session_events client_session_events = { PW_VERSION_CLIENT_SESSION_METHODS, .set_param = client_session_set_param, .link_set_param = client_session_link_set_param, .link_request_state = client_session_link_request_state, }; static void proxy_client_session_bound(void *data, uint32_t id) { struct impl *impl = data; struct pw_session_info info; impl->session->session_id = id; spa_zero(info); info.version = PW_VERSION_SESSION_INFO; info.id = id; pw_log_debug("got session id:%d", id); pw_client_session_update(impl->session->client_session, PW_CLIENT_SESSION_UPDATE_INFO, 0, NULL, &info); /* start endpoints */ sm_bluez5_endpoint_start(impl->session); sm_alsa_endpoint_start(impl->session); sm_v4l2_endpoint_start(impl->session); sm_stream_endpoint_start(impl->session); sm_policy_ep_start(impl->session); } static const struct pw_proxy_events proxy_client_session_events = { PW_VERSION_PROXY_EVENTS, .bound = proxy_client_session_bound, }; static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->listener); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .destroy = session_destroy, }; int sm_session_manager_start(struct sm_media_session *session) { struct impl *impl; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; impl->context = session->context; sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); session->client_session = (struct pw_client_session *) sm_media_session_create_object(impl->session, "client-session", PW_TYPE_INTERFACE_ClientSession, PW_VERSION_CLIENT_SESSION, NULL, 0); pw_proxy_add_listener((struct pw_proxy*)session->client_session, &impl->proxy_client_session_listener, &proxy_client_session_events, impl); pw_client_session_add_listener(session->client_session, &impl->client_session_listener, &client_session_events, impl); return 0; } 07070100000076000081A40000000000000000000000016178A88C0000424A000000000000000000000000000000000000002A00000000media-session-0.4.1/src/stream-endpoint.c/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/utils/result.h> #include <spa/utils/string.h> #include <spa/param/audio/format-utils.h> #include <spa/param/props.h> #include <spa/debug/pod.h> #include "pipewire/pipewire.h" #include "pipewire/extensions/session-manager.h" #include "media-session.h" /** \page page_media_session_module_stream_endpoint Media Session Module: Stream Endpoint */ #define NAME "stream-endpoint" #define SESSION_KEY "stream-endpoint" #define DEFAULT_CHANNELS 2 #define DEFAULT_SAMPLERATE 48000 PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct endpoint; struct impl { struct sm_media_session *session; struct spa_hook listener; }; struct node { struct sm_node *obj; struct spa_hook listener; struct impl *impl; uint32_t id; enum pw_direction direction; char *media; struct endpoint *endpoint; struct spa_audio_info format; }; struct stream { struct endpoint *endpoint; struct spa_list link; struct pw_properties *props; struct pw_endpoint_stream_info info; struct spa_audio_info format; unsigned int active:1; }; struct endpoint { struct impl *impl; struct pw_properties *props; struct node *node; struct pw_client_endpoint *client_endpoint; struct spa_hook client_endpoint_listener; struct spa_hook proxy_listener; struct pw_endpoint_info info; struct spa_param_info params[5]; struct spa_list stream_list; }; static int client_endpoint_set_session_id(void *object, uint32_t id) { struct endpoint *endpoint = object; endpoint->info.session_id = id; return 0; } static int client_endpoint_set_param(void *object, uint32_t id, uint32_t flags, const struct spa_pod *param) { struct endpoint *endpoint = object; struct impl *impl = endpoint->impl; struct node *node = endpoint->node; pw_log_debug("%p: node %d set param %d", impl, node->obj->obj.id, id); return pw_node_set_param((struct pw_node*)node->obj->obj.proxy, id, flags, param); } static int client_endpoint_stream_set_param(void *object, uint32_t stream_id, uint32_t id, uint32_t flags, const struct spa_pod *param) { return -ENOTSUP; } static int stream_set_active(struct stream *stream, bool active) { struct endpoint *endpoint = stream->endpoint; struct node *node = endpoint->node; char buf[1024]; struct spa_pod_builder b = { 0, }; struct spa_pod *param; if (stream->active == active) return 0; if (active) { stream->format = node->format; switch (stream->format.media_type) { case SPA_MEDIA_TYPE_audio: switch (stream->format.media_subtype) { case SPA_MEDIA_SUBTYPE_raw: stream->format.info.raw.rate = 48000; spa_pod_builder_init(&b, buf, sizeof(buf)); param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &stream->format.info.raw); param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(endpoint->info.direction), SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp), SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(false), SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, param); pw_node_set_param((struct pw_node*)node->obj->obj.proxy, SPA_PARAM_PortConfig, 0, param); break; default: break; } default: break; } } stream->active = active; return 0; } static int client_endpoint_create_link(void *object, const struct spa_dict *props) { struct endpoint *endpoint = object; struct impl *impl = endpoint->impl; struct pw_properties *p; struct stream *stream; int res; pw_log_debug("create link"); if (props == NULL) return -EINVAL; if (spa_list_is_empty(&endpoint->stream_list)) return -EIO; /* FIXME take first stream */ stream = spa_list_first(&endpoint->stream_list, struct stream, link); stream_set_active(stream, true); p = pw_properties_new_dict(props); if (p == NULL) return -errno; if (endpoint->info.direction == PW_DIRECTION_OUTPUT) { const char *str; struct sm_object *obj; pw_properties_setf(p, PW_KEY_LINK_OUTPUT_NODE, "%d", endpoint->node->id); pw_properties_setf(p, PW_KEY_LINK_OUTPUT_PORT, "-1"); str = spa_dict_lookup(props, PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT); if (str == NULL) { pw_log_warn("%p: no target endpoint given", impl); res = -EINVAL; goto exit; } obj = sm_media_session_find_object(impl->session, atoi(str)); if (obj == NULL || !spa_streq(obj->type, PW_TYPE_INTERFACE_Endpoint)) { pw_log_warn("%p: could not find endpoint %s (%p)", impl, str, obj); res = -EINVAL; goto exit; } pw_endpoint_create_link((struct pw_endpoint*)obj->proxy, &p->dict); } else { pw_properties_setf(p, PW_KEY_LINK_INPUT_NODE, "%d", endpoint->node->id); pw_properties_setf(p, PW_KEY_LINK_INPUT_PORT, "-1"); sm_media_session_create_links(impl->session, &p->dict); } res = 0; exit: pw_properties_free(p); return res; } static const struct pw_client_endpoint_events client_endpoint_events = { PW_VERSION_CLIENT_ENDPOINT_EVENTS, .set_session_id = client_endpoint_set_session_id, .set_param = client_endpoint_set_param, .stream_set_param = client_endpoint_stream_set_param, .create_link = client_endpoint_create_link, }; static struct stream *endpoint_add_stream(struct endpoint *endpoint) { struct stream *s; struct pw_properties *props = endpoint->props; struct node *node = endpoint->node; const char *str; s = calloc(1, sizeof(*s)); if (s == NULL) return NULL; s->endpoint = endpoint; s->props = pw_properties_new(NULL, NULL); if ((str = pw_properties_get(props, PW_KEY_MEDIA_CLASS)) != NULL) pw_properties_set(s->props, PW_KEY_MEDIA_CLASS, str); if (node->direction == PW_DIRECTION_OUTPUT) pw_properties_set(s->props, PW_KEY_ENDPOINT_STREAM_NAME, "Playback"); else pw_properties_set(s->props, PW_KEY_ENDPOINT_STREAM_NAME, "Capture"); s->info.version = PW_VERSION_ENDPOINT_STREAM_INFO; s->info.id = 0; s->info.endpoint_id = endpoint->info.id; s->info.name = (char*)pw_properties_get(s->props, PW_KEY_ENDPOINT_STREAM_NAME); s->info.change_mask = PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS; s->info.props = &s->props->dict; spa_list_append(&endpoint->stream_list, &s->link); pw_log_debug("stream %d", node->id); pw_client_endpoint_stream_update(endpoint->client_endpoint, s->info.id, PW_CLIENT_ENDPOINT_STREAM_UPDATE_INFO, 0, NULL, &s->info); return s; } static void destroy_stream(struct stream *stream) { struct endpoint *endpoint = stream->endpoint; pw_client_endpoint_stream_update(endpoint->client_endpoint, stream->info.id, PW_CLIENT_ENDPOINT_STREAM_UPDATE_DESTROYED, 0, NULL, &stream->info); pw_properties_free(stream->props); spa_list_remove(&stream->link); free(stream); } static void complete_endpoint(void *data) { struct endpoint *endpoint = data; struct impl *impl = endpoint->impl; struct node *node = endpoint->node; struct sm_param *p; pw_log_debug("%p: endpoint %p", impl, endpoint); spa_list_for_each(p, &node->obj->param_list, link) { struct spa_audio_info info = { 0, }; switch (p->id) { case SPA_PARAM_EnumFormat: if (spa_format_parse(p->param, &info.media_type, &info.media_subtype) < 0) continue; if (info.media_type != SPA_MEDIA_TYPE_audio || info.media_subtype != SPA_MEDIA_SUBTYPE_raw) continue; spa_pod_object_fixate((struct spa_pod_object*)p->param); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, p->param); if (spa_format_audio_raw_parse(p->param, &info.info.raw) < 0) continue; if (node->format.info.raw.channels < info.info.raw.channels) node->format = info; break; default: break; } } pw_client_endpoint_update(endpoint->client_endpoint, PW_CLIENT_ENDPOINT_UPDATE_INFO, 0, NULL, &endpoint->info); endpoint_add_stream(endpoint); } static void update_params(void *data) { uint32_t n_params; const struct spa_pod **params; struct endpoint *endpoint = data; struct impl *impl = endpoint->impl; struct node *node = endpoint->node; struct sm_param *p; pw_log_debug("%p: endpoint %p", impl, endpoint); params = alloca(sizeof(struct spa_pod *) * node->obj->n_params); n_params = 0; spa_list_for_each(p, &node->obj->param_list, link) { switch (p->id) { case SPA_PARAM_Props: case SPA_PARAM_PropInfo: params[n_params++] = p->param; break; default: break; } } pw_client_endpoint_update(endpoint->client_endpoint, PW_CLIENT_ENDPOINT_UPDATE_PARAMS | PW_CLIENT_ENDPOINT_UPDATE_INFO, n_params, params, &endpoint->info); } static void proxy_destroy(void *data) { struct endpoint *endpoint = data; struct stream *s; spa_list_consume(s, &endpoint->stream_list, link) destroy_stream(s); pw_properties_free(endpoint->props); } static void proxy_bound(void *data, uint32_t id) { struct endpoint *endpoint = data; endpoint->info.id = id; } static const struct pw_proxy_events proxy_events = { PW_VERSION_PROXY_EVENTS, .destroy = proxy_destroy, .bound = proxy_bound, }; static struct endpoint *create_endpoint(struct node *node) { struct impl *impl = node->impl; struct pw_properties *props; struct endpoint *endpoint; struct pw_proxy *proxy; const char *str, *media_class = NULL, *name = NULL; uint32_t subscribe[4], n_subscribe = 0; props = pw_properties_new(NULL, NULL); if (props == NULL) return NULL; if (node->obj->info && node->obj->info->props) { struct spa_dict *dict = node->obj->info->props; if ((media_class = spa_dict_lookup(dict, PW_KEY_MEDIA_CLASS)) != NULL) pw_properties_set(props, PW_KEY_MEDIA_CLASS, media_class); if ((name = spa_dict_lookup(dict, PW_KEY_MEDIA_NAME)) != NULL) pw_properties_set(props, PW_KEY_ENDPOINT_NAME, name); if ((str = spa_dict_lookup(dict, PW_KEY_OBJECT_ID)) != NULL) pw_properties_set(props, PW_KEY_NODE_ID, str); if ((str = spa_dict_lookup(dict, PW_KEY_CLIENT_ID)) != NULL) pw_properties_set(props, PW_KEY_ENDPOINT_CLIENT_ID, str); if ((str = spa_dict_lookup(dict, PW_KEY_NODE_AUTOCONNECT)) != NULL) pw_properties_set(props, PW_KEY_ENDPOINT_AUTOCONNECT, str); if ((str = spa_dict_lookup(dict, PW_KEY_NODE_TARGET)) != NULL) pw_properties_set(props, PW_KEY_NODE_TARGET, str); if ((str = spa_dict_lookup(dict, PW_KEY_ENDPOINT_TARGET)) != NULL) pw_properties_set(props, PW_KEY_ENDPOINT_TARGET, str); } proxy = sm_media_session_create_object(impl->session, "client-endpoint", PW_TYPE_INTERFACE_ClientEndpoint, PW_VERSION_CLIENT_ENDPOINT, &props->dict, sizeof(*endpoint)); if (proxy == NULL) { pw_properties_free(props); return NULL; } endpoint = pw_proxy_get_user_data(proxy); endpoint->impl = impl; endpoint->node = node; endpoint->props = props; endpoint->client_endpoint = (struct pw_client_endpoint *) proxy; endpoint->info.version = PW_VERSION_ENDPOINT_INFO; endpoint->info.name = (char*)pw_properties_get(props, PW_KEY_ENDPOINT_NAME); endpoint->info.media_class = (char*)pw_properties_get(props, PW_KEY_MEDIA_CLASS); endpoint->info.session_id = impl->session->session->obj.id; endpoint->info.direction = node->direction; endpoint->info.flags = 0; endpoint->info.change_mask = PW_ENDPOINT_CHANGE_MASK_STREAMS | PW_ENDPOINT_CHANGE_MASK_SESSION | PW_ENDPOINT_CHANGE_MASK_PROPS | PW_ENDPOINT_CHANGE_MASK_PARAMS; endpoint->info.n_streams = 1; endpoint->info.props = &endpoint->props->dict; endpoint->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); endpoint->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); endpoint->info.params = endpoint->params; endpoint->info.n_params = 2; spa_list_init(&endpoint->stream_list); pw_proxy_add_listener(proxy, &endpoint->proxy_listener, &proxy_events, endpoint); pw_client_endpoint_add_listener(endpoint->client_endpoint, &endpoint->client_endpoint_listener, &client_endpoint_events, endpoint); subscribe[n_subscribe++] = SPA_PARAM_EnumFormat; subscribe[n_subscribe++] = SPA_PARAM_Props; subscribe[n_subscribe++] = SPA_PARAM_PropInfo; pw_log_debug("%p: node %p proxy %p subscribe %d params", impl, node->obj, node->obj->obj.proxy, n_subscribe); pw_node_subscribe_params((struct pw_node*)node->obj->obj.proxy, subscribe, n_subscribe); sm_media_session_sync(impl->session, complete_endpoint, endpoint); return endpoint; } static void destroy_endpoint(struct endpoint *endpoint) { pw_proxy_destroy((struct pw_proxy*)endpoint->client_endpoint); } static void object_update(void *data) { struct node *node = data; struct impl *impl = node->impl; pw_log_debug("%p: node %p endpoint %p %08x", impl, node, node->endpoint, node->obj->obj.changed); if (node->endpoint == NULL && node->obj->obj.avail & SM_OBJECT_CHANGE_MASK_PROPERTIES) node->endpoint = create_endpoint(node); if (node->endpoint && node->obj->obj.changed & SM_NODE_CHANGE_MASK_PARAMS) update_params(node->endpoint); } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static int handle_node(struct impl *impl, struct sm_object *obj) { const char *media_class; enum pw_direction direction; struct node *node; media_class = obj->props ? pw_properties_get(obj->props, PW_KEY_MEDIA_CLASS) : NULL; pw_log_debug("%p: node "PW_KEY_MEDIA_CLASS" %s", impl, media_class); if (media_class == NULL) return 0; if (!spa_strstartswith(media_class, "Stream/")) return 0; media_class += strlen("Stream/"); if (spa_strstartswith(media_class, "Output/")) { direction = PW_DIRECTION_OUTPUT; media_class += strlen("Output/"); } else if (spa_strstartswith(media_class, "Input/")) { direction = PW_DIRECTION_INPUT; media_class += strlen("Input/"); } else return 0; node = sm_object_add_data(obj, SESSION_KEY, sizeof(struct node)); node->obj = (struct sm_node*)obj; node->impl = impl; node->id = obj->id; node->direction = direction; node->media = strdup(media_class); pw_log_debug("%p: node %d is stream %d:%s", impl, node->id, node->direction, node->media); sm_object_add_listener(obj, &node->listener, &object_events, node); return 1; } static void destroy_node(struct impl *impl, struct node *node) { if (node->endpoint) destroy_endpoint(node->endpoint); free(node->media); spa_hook_remove(&node->listener); sm_object_remove_data((struct sm_object*)node->obj, SESSION_KEY); } static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; int res; if (spa_streq(object->type, PW_TYPE_INTERFACE_Node)) res = handle_node(impl, object); else res = 0; if (res < 0) { pw_log_warn("%p: can't handle global %d: %s", impl, object->id, spa_strerror(res)); } } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; if (spa_streq(object->type, PW_TYPE_INTERFACE_Node)) { struct node *node; if ((node = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_node(impl, node); } } static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->listener); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .destroy = session_destroy, }; int sm_stream_endpoint_start(struct sm_media_session *session) { struct impl *impl; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; sm_media_session_add_listener(session, &impl->listener, &session_events, impl); return 0; } 07070100000077000081A40000000000000000000000016178A88C00000772000000000000000000000000000000000000003100000000media-session-0.4.1/src/streams-follow-default.c/* PipeWire * * Copyright © 2021 Pauli Virtanen * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ /* * Instruct policy-node to move streams when the default sink/sources (either explicitly set * via metadata, or determined from priority) changes, and the stream does not have an * explicitly specified target node. * * This is done by just setting a session property flag, and policy-node does the rest. */ #include "config.h" #include "pipewire/pipewire.h" #include "pipewire/extensions/metadata.h" #include "media-session.h" /** \page page_media_session_module_stream_follow_default Media Session Module: Stream Follow Default */ #define KEY_NAME "policy-node.streams-follow-default" int sm_streams_follow_default_start(struct sm_media_session *session) { pw_properties_set(session->props, KEY_NAME, "true"); return 0; } 07070100000078000081A40000000000000000000000016178A88C00001A9E000000000000000000000000000000000000002700000000media-session-0.4.1/src/suspend-node.c/* PipeWire * * Copyright © 2020 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/utils/string.h> #include <spa/param/props.h> #include <spa/debug/pod.h> #include "pipewire/pipewire.h" #include "media-session.h" /** \page page_media_session_module_suspend_node Media Session Module: Suspend Node */ #define NAME "suspend-node" #define SESSION_KEY "suspend-node" #define DEFAULT_IDLE_SECONDS 3 PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct impl { struct timespec now; struct sm_media_session *session; struct spa_hook listener; struct pw_context *context; struct spa_list node_list; int seq; }; struct node { struct sm_node *obj; uint32_t id; struct impl *impl; struct spa_list link; /**< link in impl node_list */ enum pw_direction direction; struct spa_hook listener; struct spa_source *idle_timeout; }; static void remove_idle_timeout(struct node *node) { struct impl *impl = node->impl; struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); if (node->idle_timeout) { pw_loop_destroy_source(main_loop, node->idle_timeout); node->idle_timeout = NULL; } } static void idle_timeout(void *data, uint64_t expirations) { struct node *node = data; struct impl *impl = node->impl; struct spa_command *cmd = &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Suspend); pw_log_info("%p: node %d suspend", impl, node->id); remove_idle_timeout(node); pw_node_send_command((struct pw_node*)node->obj->obj.proxy, cmd); sm_object_release(&node->obj->obj); } static void add_idle_timeout(struct node *node) { struct timespec value; struct impl *impl = node->impl; struct pw_loop *main_loop = pw_context_get_main_loop(impl->context); const char *str; if (node->obj->info && node->obj->info->props && (str = spa_dict_lookup(node->obj->info->props, "session.suspend-timeout-seconds")) != NULL) value.tv_sec = atoi(str); else value.tv_sec = DEFAULT_IDLE_SECONDS; if (value.tv_sec == 0) return; if (node->idle_timeout == NULL) node->idle_timeout = pw_loop_add_timer(main_loop, idle_timeout, node); value.tv_nsec = 0; pw_loop_update_timer(main_loop, node->idle_timeout, &value, NULL, false); } static int on_node_idle(struct impl *impl, struct node *node) { pw_log_debug("%p: node %d idle", impl, node->id); add_idle_timeout(node); return 0; } static int on_node_running(struct impl *impl, struct node *node) { pw_log_debug("%p: node %d running", impl, node->id); sm_object_acquire(&node->obj->obj); remove_idle_timeout(node); return 0; } static void object_update(void *data) { struct node *node = data; struct impl *impl = node->impl; pw_log_debug("%p: node %p %08x", impl, node, node->obj->obj.changed); if (node->obj->obj.changed & SM_NODE_CHANGE_MASK_INFO) { const struct pw_node_info *info = node->obj->info; if (info->change_mask & PW_NODE_CHANGE_MASK_STATE) { switch (info->state) { case PW_NODE_STATE_ERROR: case PW_NODE_STATE_IDLE: on_node_idle(impl, node); break; case PW_NODE_STATE_RUNNING: on_node_running(impl, node); break; case PW_NODE_STATE_SUSPENDED: break; default: break; } } } } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static int handle_node(struct impl *impl, struct sm_object *object) { struct node *node; const char *media_class; media_class = object->props ? pw_properties_get(object->props, PW_KEY_MEDIA_CLASS) : NULL; if (media_class == NULL) return 0; if (!spa_strstartswith(media_class, "Audio/") && (!spa_strstartswith(media_class, "Video/"))) return 0; node = sm_object_add_data(object, SESSION_KEY, sizeof(struct node)); node->obj = (struct sm_node*)object; node->impl = impl; node->id = object->id; spa_list_append(&impl->node_list, &node->link); node->obj->obj.mask |= SM_NODE_CHANGE_MASK_INFO; sm_object_add_listener(&node->obj->obj, &node->listener, &object_events, node); return 1; } static void destroy_node(struct impl *impl, struct node *node) { remove_idle_timeout(node); spa_list_remove(&node->link); spa_hook_remove(&node->listener); sm_object_remove_data((struct sm_object*)node->obj, SESSION_KEY); } static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; int res; if (spa_streq(object->type, PW_TYPE_INTERFACE_Node)) res = handle_node(impl, object); else res = 0; if (res < 0) pw_log_warn("%p: can't handle global %d", impl, object->id); } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; pw_log_debug("%p: remove global '%d'", impl, object->id); if (spa_streq(object->type, PW_TYPE_INTERFACE_Node)) { struct node *node; if ((node = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_node(impl, node); } } static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->listener); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .destroy = session_destroy, }; int sm_suspend_node_start(struct sm_media_session *session) { struct impl *impl; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; impl->context = session->context; spa_list_init(&impl->node_list); sm_media_session_add_listener(impl->session, &impl->listener, &session_events, impl); return 0; } 07070100000079000081A40000000000000000000000016178A88C00004416000000000000000000000000000000000000002800000000media-session-0.4.1/src/v4l2-endpoint.c/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/utils/result.h> #include <spa/utils/names.h> #include <spa/utils/keys.h> #include <spa/utils/string.h> #include <spa/param/video/format-utils.h> #include <spa/param/props.h> #include <spa/debug/dict.h> #include <spa/debug/pod.h> #include "pipewire/pipewire.h" #include <pipewire/extensions/session-manager.h> #include "media-session.h" /** \page page_media_session_module_v4l2_endpoint Media Session Module: V4L2 Endpoint */ #define NAME "v4l2-endpoint" #define SESSION_KEY "v4l2-endpoint" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct endpoint { struct spa_list link; struct impl *impl; struct pw_properties *props; struct node *node; struct spa_hook listener; struct pw_client_endpoint *client_endpoint; struct spa_hook proxy_listener; struct spa_hook client_endpoint_listener; struct pw_endpoint_info info; struct spa_param_info params[5]; struct spa_list stream_list; }; struct stream { struct spa_list link; struct endpoint *endpoint; struct pw_properties *props; struct pw_endpoint_stream_info info; unsigned int active:1; }; struct node { struct impl *impl; struct sm_node *node; struct device *device; struct endpoint *endpoint; }; struct device { struct impl *impl; uint32_t id; struct sm_device *device; struct spa_hook listener; struct spa_list endpoint_list; }; struct impl { struct sm_media_session *session; struct spa_hook listener; }; static int client_endpoint_set_session_id(void *object, uint32_t id) { struct endpoint *endpoint = object; endpoint->info.session_id = id; return 0; } static int client_endpoint_set_param(void *object, uint32_t id, uint32_t flags, const struct spa_pod *param) { struct endpoint *endpoint = object; struct impl *impl = endpoint->impl; pw_log_debug("%p: endpoint %p set param %d", impl, endpoint, id); return pw_node_set_param((struct pw_node*)endpoint->node->node->obj.proxy, id, flags, param); } static int client_endpoint_stream_set_param(void *object, uint32_t stream_id, uint32_t id, uint32_t flags, const struct spa_pod *param) { return -ENOTSUP; } static int stream_set_active(struct endpoint *endpoint, struct stream *stream, bool active) { if (stream->active == active) return 0; stream->active = active; return 0; } static int client_endpoint_create_link(void *object, const struct spa_dict *props) { struct endpoint *endpoint = object; struct impl *impl = endpoint->impl; struct pw_properties *p; int res; pw_log_debug("%p: endpoint %p", impl, endpoint); if (props == NULL) return -EINVAL; p = pw_properties_new_dict(props); if (p == NULL) return -errno; if (endpoint->info.direction == PW_DIRECTION_OUTPUT) { const char *str; struct sm_object *obj; str = spa_dict_lookup(props, PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT); if (str == NULL) { pw_log_warn("%p: no target endpoint given", impl); res = -EINVAL; goto exit; } obj = sm_media_session_find_object(impl->session, atoi(str)); if (obj == NULL || !spa_streq(obj->type, PW_TYPE_INTERFACE_Endpoint)) { pw_log_warn("%p: could not find endpoint %s (%p)", impl, str, obj); res = -EINVAL; goto exit; } pw_properties_setf(p, PW_KEY_LINK_OUTPUT_NODE, "%d", endpoint->node->node->info->id); pw_properties_setf(p, PW_KEY_LINK_OUTPUT_PORT, "-1"); pw_endpoint_create_link((struct pw_endpoint*)obj->proxy, &p->dict); } else { pw_properties_setf(p, PW_KEY_LINK_INPUT_NODE, "%d", endpoint->node->node->info->id); pw_properties_setf(p, PW_KEY_LINK_INPUT_PORT, "-1"); sm_media_session_create_links(impl->session, &p->dict); } res = 0; exit: pw_properties_free(p); return res; } static const struct pw_client_endpoint_events client_endpoint_events = { PW_VERSION_CLIENT_ENDPOINT_EVENTS, .set_session_id = client_endpoint_set_session_id, .set_param = client_endpoint_set_param, .stream_set_param = client_endpoint_stream_set_param, .create_link = client_endpoint_create_link, }; static struct stream *endpoint_add_stream(struct endpoint *endpoint) { struct stream *s; const char *str; s = calloc(1, sizeof(*s)); if (s == NULL) return NULL; s->props = pw_properties_new(NULL, NULL); s->endpoint = endpoint; if ((str = pw_properties_get(endpoint->props, PW_KEY_MEDIA_CLASS)) != NULL) pw_properties_set(s->props, PW_KEY_MEDIA_CLASS, str); if ((str = pw_properties_get(endpoint->props, PW_KEY_PRIORITY_SESSION)) != NULL) pw_properties_set(s->props, PW_KEY_PRIORITY_SESSION, str); if (endpoint->info.direction == PW_DIRECTION_OUTPUT) { pw_properties_set(s->props, PW_KEY_ENDPOINT_STREAM_NAME, "Capture"); } else { pw_properties_set(s->props, PW_KEY_ENDPOINT_STREAM_NAME, "Playback"); } s->info.version = PW_VERSION_ENDPOINT_STREAM_INFO; s->info.id = endpoint->info.n_streams; s->info.endpoint_id = endpoint->info.id; s->info.name = (char*)pw_properties_get(s->props, PW_KEY_ENDPOINT_STREAM_NAME); s->info.change_mask = PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS; s->info.props = &s->props->dict; pw_log_debug("stream %d", s->info.id); pw_client_endpoint_stream_update(endpoint->client_endpoint, s->info.id, PW_CLIENT_ENDPOINT_STREAM_UPDATE_INFO, 0, NULL, &s->info); spa_list_append(&endpoint->stream_list, &s->link); endpoint->info.n_streams++; return s; } static void destroy_stream(struct stream *stream) { struct endpoint *endpoint = stream->endpoint; pw_client_endpoint_stream_update(endpoint->client_endpoint, stream->info.id, PW_CLIENT_ENDPOINT_STREAM_UPDATE_DESTROYED, 0, NULL, &stream->info); spa_list_remove(&stream->link); endpoint->info.n_streams--; pw_properties_free(stream->props); free(stream); } static void update_params(void *data) { uint32_t n_params; const struct spa_pod **params; struct endpoint *endpoint = data; struct sm_node *node = endpoint->node->node; struct sm_param *p; pw_log_debug("%p: endpoint", endpoint); params = alloca(sizeof(struct spa_pod *) * node->n_params); n_params = 0; spa_list_for_each(p, &node->param_list, link) { switch (p->id) { case SPA_PARAM_Props: case SPA_PARAM_PropInfo: params[n_params++] = p->param; break; default: break; } } pw_client_endpoint_update(endpoint->client_endpoint, PW_CLIENT_ENDPOINT_UPDATE_PARAMS | PW_CLIENT_ENDPOINT_UPDATE_INFO, n_params, params, &endpoint->info); } static struct endpoint *create_endpoint(struct node *node); static void object_update(void *data) { struct endpoint *endpoint = data; struct impl *impl = endpoint->impl; struct sm_node *node = endpoint->node->node; pw_log_debug("%p: endpoint %p", impl, endpoint); if (node->obj.changed & SM_NODE_CHANGE_MASK_PARAMS) update_params(endpoint); } static const struct sm_object_events object_events = { SM_VERSION_OBJECT_EVENTS, .update = object_update }; static void complete_endpoint(void *data) { struct endpoint *endpoint = data; struct stream *stream; struct sm_param *p; pw_log_debug("endpoint %p: complete", endpoint); spa_list_for_each(p, &endpoint->node->node->param_list, link) { struct spa_video_info info = { 0, }; if (p->id != SPA_PARAM_EnumFormat) continue; if (spa_format_parse(p->param, &info.media_type, &info.media_subtype) < 0) continue; if (info.media_type != SPA_MEDIA_TYPE_video || info.media_subtype != SPA_MEDIA_SUBTYPE_raw) continue; spa_pod_object_fixate((struct spa_pod_object*)p->param); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_pod(2, NULL, p->param); if (spa_format_video_raw_parse(p->param, &info.info.raw) < 0) continue; } pw_client_endpoint_update(endpoint->client_endpoint, PW_CLIENT_ENDPOINT_UPDATE_INFO, 0, NULL, &endpoint->info); stream = endpoint_add_stream(endpoint); stream_set_active(endpoint, stream, true); sm_object_add_listener(&endpoint->node->node->obj, &endpoint->listener, &object_events, endpoint); } static void proxy_destroy(void *data) { struct endpoint *endpoint = data; struct stream *s; pw_log_debug("endpoint %p: destroy", endpoint); spa_list_consume(s, &endpoint->stream_list, link) destroy_stream(s); pw_properties_free(endpoint->props); spa_list_remove(&endpoint->link); spa_hook_remove(&endpoint->proxy_listener); spa_hook_remove(&endpoint->client_endpoint_listener); endpoint->client_endpoint = NULL; } static void proxy_bound(void *data, uint32_t id) { struct endpoint *endpoint = data; endpoint->info.id = id; } static const struct pw_proxy_events proxy_events = { PW_VERSION_PROXY_EVENTS, .destroy = proxy_destroy, .bound = proxy_bound, }; static struct endpoint *create_endpoint(struct node *node) { struct impl *impl = node->impl; struct device *device = node->device; struct pw_properties *props; struct endpoint *endpoint; struct pw_proxy *proxy; const char *str, *media_class = NULL, *name = NULL; uint32_t subscribe[4], n_subscribe = 0; struct pw_properties *pr = node->node->obj.props; enum pw_direction direction; if (pr == NULL) { errno = EINVAL; return NULL; } if ((media_class = pw_properties_get(pr, PW_KEY_MEDIA_CLASS)) == NULL) { errno = EINVAL; return NULL; } if (strstr(media_class, "Source") != NULL) { direction = PW_DIRECTION_OUTPUT; } else if (strstr(media_class, "Sink") != NULL) { direction = PW_DIRECTION_INPUT; } else { errno = EINVAL; return NULL; } props = pw_properties_new(NULL, NULL); if (props == NULL) return NULL; pw_properties_set(props, PW_KEY_MEDIA_CLASS, media_class); if ((str = pw_properties_get(pr, PW_KEY_PRIORITY_SESSION)) != NULL) pw_properties_set(props, PW_KEY_PRIORITY_SESSION, str); if ((name = pw_properties_get(pr, PW_KEY_NODE_DESCRIPTION)) != NULL) { pw_properties_set(props, PW_KEY_ENDPOINT_NAME, name); } if ((str = pw_properties_get(pr, PW_KEY_DEVICE_ICON_NAME)) != NULL) pw_properties_set(props, PW_KEY_ENDPOINT_ICON_NAME, str); proxy = sm_media_session_create_object(impl->session, "client-endpoint", PW_TYPE_INTERFACE_ClientEndpoint, PW_VERSION_CLIENT_ENDPOINT, &props->dict, sizeof(*endpoint)); if (proxy == NULL) { pw_properties_free(props); return NULL; } endpoint = pw_proxy_get_user_data(proxy); endpoint->impl = impl; endpoint->node = node; endpoint->props = props; endpoint->client_endpoint = (struct pw_client_endpoint *) proxy; endpoint->info.version = PW_VERSION_ENDPOINT_INFO; endpoint->info.name = (char*)pw_properties_get(endpoint->props, PW_KEY_ENDPOINT_NAME); endpoint->info.media_class = (char*)pw_properties_get(endpoint->props, PW_KEY_MEDIA_CLASS); endpoint->info.session_id = impl->session->session->obj.id; endpoint->info.direction = direction; endpoint->info.flags = 0; endpoint->info.change_mask = PW_ENDPOINT_CHANGE_MASK_STREAMS | PW_ENDPOINT_CHANGE_MASK_SESSION | PW_ENDPOINT_CHANGE_MASK_PROPS | PW_ENDPOINT_CHANGE_MASK_PARAMS; endpoint->info.n_streams = 0; endpoint->info.props = &endpoint->props->dict; endpoint->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); endpoint->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); endpoint->info.params = endpoint->params; endpoint->info.n_params = 2; spa_list_init(&endpoint->stream_list); pw_log_debug("%p: new endpoint %p for v4l2 node %p", impl, endpoint, node); pw_proxy_add_listener(proxy, &endpoint->proxy_listener, &proxy_events, endpoint); pw_client_endpoint_add_listener(endpoint->client_endpoint, &endpoint->client_endpoint_listener, &client_endpoint_events, endpoint); subscribe[n_subscribe++] = SPA_PARAM_EnumFormat; subscribe[n_subscribe++] = SPA_PARAM_Props; subscribe[n_subscribe++] = SPA_PARAM_PropInfo; pw_log_debug("%p: endpoint %p proxy %p subscribe %d params", impl, endpoint, node->node->obj.proxy, n_subscribe); pw_node_subscribe_params((struct pw_node*)node->node->obj.proxy, subscribe, n_subscribe); spa_list_append(&device->endpoint_list, &endpoint->link); sm_media_session_sync(impl->session, complete_endpoint, endpoint); return endpoint; } static void destroy_endpoint(struct impl *impl, struct endpoint *endpoint) { pw_log_debug("endpoint %p: destroy", endpoint); if (endpoint->client_endpoint) { pw_proxy_destroy((struct pw_proxy*)endpoint->client_endpoint); } } /** fallback, one stream for each node */ static int setup_v4l2_endpoint(struct device *device) { struct impl *impl = device->impl; struct sm_node *n; struct sm_device *d = device->device; pw_log_debug("%p: device %p setup", impl, d); spa_list_for_each(n, &d->node_list, link) { struct node *node; pw_log_debug("%p: device %p has node %p", impl, d, n); node = sm_object_add_data(&n->obj, SESSION_KEY, sizeof(struct node)); node->device = device; node->node = n; node->impl = impl; node->endpoint = create_endpoint(node); if (node->endpoint == NULL) return -errno; } return 0; } static int activate_device(struct device *device) { return setup_v4l2_endpoint(device); } static int deactivate_device(struct device *device) { struct impl *impl = device->impl; struct endpoint *e; pw_log_debug("%p: device %p deactivate", impl, device->device); spa_list_consume(e, &device->endpoint_list, link) destroy_endpoint(impl, e); return 0; } static void device_update(void *data) { struct device *device = data; struct impl *impl = device->impl; pw_log_debug("%p: device %p %08x %08x", impl, device, device->device->obj.avail, device->device->obj.changed); if (!SPA_FLAG_IS_SET(device->device->obj.avail, SM_DEVICE_CHANGE_MASK_INFO | SM_DEVICE_CHANGE_MASK_NODES)) return; if (SPA_FLAG_IS_SET(device->device->obj.changed, SM_DEVICE_CHANGE_MASK_NODES)) { activate_device(device); } } static const struct sm_object_events device_events = { SM_VERSION_OBJECT_EVENTS, .update = device_update }; static int handle_device(struct impl *impl, struct sm_object *obj) { const char *media_class, *str; struct device *device; if (obj->props == NULL) return 0; media_class = pw_properties_get(obj->props, PW_KEY_MEDIA_CLASS); str = pw_properties_get(obj->props, PW_KEY_DEVICE_API); pw_log_debug("%p: device "PW_KEY_MEDIA_CLASS":%s api:%s", impl, media_class, str); if (!spa_strstartswith(media_class, "Video/")) return 0; if (!spa_streq(str, "v4l2")) return 0; device = sm_object_add_data(obj, SESSION_KEY, sizeof(struct device)); device->impl = impl; device->id = obj->id; device->device = (struct sm_device*)obj; spa_list_init(&device->endpoint_list); pw_log_debug("%p: found v4l2 device %d media_class %s", impl, obj->id, media_class); sm_object_add_listener(obj, &device->listener, &device_events, device); return 0; } static void destroy_device(struct impl *impl, struct device *device) { deactivate_device(device); spa_hook_remove(&device->listener); sm_object_remove_data((struct sm_object*)device->device, SESSION_KEY); } static void session_create(void *data, struct sm_object *object) { struct impl *impl = data; int res; if (spa_streq(object->type, PW_TYPE_INTERFACE_Device)) res = handle_device(impl, object); else res = 0; if (res < 0) { pw_log_warn("%p: can't handle global %d: %s", impl, object->id, spa_strerror(res)); } } static void session_remove(void *data, struct sm_object *object) { struct impl *impl = data; if (spa_streq(object->type, PW_TYPE_INTERFACE_Device)) { struct device *device; if ((device = sm_object_get_data(object, SESSION_KEY)) != NULL) destroy_device(impl, device); } } static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->listener); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .create = session_create, .remove = session_remove, .destroy = session_destroy, }; int sm_v4l2_endpoint_start(struct sm_media_session *session) { struct impl *impl; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->session = session; sm_media_session_add_listener(session, &impl->listener, &session_events, impl); return 0; } 0707010000007A000081A40000000000000000000000016178A88C000038EA000000000000000000000000000000000000002700000000media-session-0.4.1/src/v4l2-monitor.c/* PipeWire * * Copyright © 2019 Wim Taymans * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <string.h> #include <stdio.h> #include <errno.h> #include <math.h> #include <time.h> #include "config.h" #include <spa/monitor/device.h> #include <spa/node/node.h> #include <spa/utils/hook.h> #include <spa/utils/names.h> #include <spa/utils/result.h> #include <spa/utils/string.h> #include <spa/param/props.h> #include <spa/debug/dict.h> #include <spa/pod/builder.h> #include "pipewire/pipewire.h" #include "media-session.h" /** \page page_media_session_module_v4l2_monitor Media Session Module: V4L2 Monitor */ #define NAME "v4l2-monitor" #define SESSION_CONF "v4l2-monitor.conf" PW_LOG_TOPIC_STATIC(mod_topic, "ms.mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic struct device; struct node { struct impl *impl; struct device *device; struct spa_list link; uint32_t id; struct pw_properties *props; struct pw_proxy *proxy; struct spa_node *node; }; struct device { struct impl *impl; struct spa_list link; uint32_t id; uint32_t device_id; int priority; int profile; struct pw_properties *props; struct spa_handle *handle; struct spa_device *device; struct spa_hook device_listener; struct sm_device *sdevice; struct spa_hook listener; unsigned int appeared:1; struct spa_list node_list; }; struct impl { struct sm_media_session *session; struct spa_hook session_listener; struct pw_properties *conf; struct spa_handle *handle; struct spa_device *monitor; struct spa_hook listener; struct spa_list device_list; }; static struct node *v4l2_find_node(struct device *dev, uint32_t id, const char *name) { struct node *node; const char *str; spa_list_for_each(node, &dev->node_list, link) { if (node->id == id) return node; if (name != NULL && (str = pw_properties_get(node->props, PW_KEY_NODE_NAME)) != NULL && spa_streq(name, str)) return node; } return NULL; } static void v4l2_update_node(struct device *dev, struct node *node, const struct spa_device_object_info *info) { pw_log_debug("update node %u", node->id); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); pw_properties_update(node->props, info->props); } static struct node *v4l2_create_node(struct device *dev, uint32_t id, const struct spa_device_object_info *info) { struct node *node; struct impl *impl = dev->impl; int i, res; const char *prefix, *str, *d, *rules; char tmp[1024]; pw_log_debug("new node %u", id); if (!spa_streq(info->type, SPA_TYPE_INTERFACE_Node)) { errno = EINVAL; return NULL; } node = calloc(1, sizeof(*node)); if (node == NULL) { res = -errno; goto exit; } node->props = pw_properties_new_dict(info->props); pw_properties_setf(node->props, PW_KEY_DEVICE_ID, "%d", dev->device_id); str = pw_properties_get(dev->props, SPA_KEY_DEVICE_NAME); if (str == NULL) str = pw_properties_get(dev->props, SPA_KEY_DEVICE_NICK); if (str == NULL) str = pw_properties_get(dev->props, SPA_KEY_DEVICE_ALIAS); if (str == NULL) str = "v4l2-device"; if (spa_strstartswith(str, "v4l2_device.")) str += 12; if (strstr(info->factory_name, "sink") != NULL) prefix = "v4l2_output"; else if (strstr(info->factory_name, "source") != NULL) prefix = "v4l2_input"; else prefix = info->factory_name; pw_properties_set(node->props, PW_KEY_NODE_NAME, sm_media_session_sanitize_name(tmp, sizeof(tmp), '_', "%s.%s", prefix, str)); for (i = 2; i <= 99; i++) { if ((d = pw_properties_get(node->props, PW_KEY_NODE_NAME)) == NULL) break; if (v4l2_find_node(dev, SPA_ID_INVALID, d) == NULL) break; pw_properties_set(node->props, PW_KEY_NODE_NAME, sm_media_session_sanitize_name(tmp, sizeof(tmp), '_', "%s.%s.%d", prefix, str, i)); } str = pw_properties_get(dev->props, SPA_KEY_DEVICE_DESCRIPTION); if (str == NULL) str = "v4l2-device"; pw_properties_set(node->props, PW_KEY_NODE_DESCRIPTION, sm_media_session_sanitize_description(tmp, sizeof(tmp), ' ', "%s", str)); pw_properties_set(node->props, PW_KEY_FACTORY_NAME, info->factory_name); if ((rules = pw_properties_get(impl->conf, "rules")) != NULL) sm_media_session_match_rules(rules, strlen(rules), node->props); node->impl = impl; node->device = dev; node->id = id; node->proxy = sm_media_session_create_object(impl->session, "spa-node-factory", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, &node->props->dict, 0); if (node->proxy == NULL) { res = -errno; goto clean_node; } spa_list_append(&dev->node_list, &node->link); return node; clean_node: pw_properties_free(node->props); free(node); exit: errno = -res; return NULL; } static void v4l2_remove_node(struct device *dev, struct node *node) { pw_log_debug("remove node %u", node->id); spa_list_remove(&node->link); pw_proxy_destroy(node->proxy); pw_properties_free(node->props); free(node); } static void v4l2_device_info(void *data, const struct spa_device_info *info) { struct device *dev = data; if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); pw_properties_update(dev->props, info->props); } static void v4l2_device_object_info(void *data, uint32_t id, const struct spa_device_object_info *info) { struct device *dev = data; struct node *node; node = v4l2_find_node(dev, id, NULL); if (info == NULL) { if (node == NULL) { pw_log_warn("device %p: unknown node %u", dev, id); return; } v4l2_remove_node(dev, node); } else if (node == NULL) { v4l2_create_node(dev, id, info); } else { v4l2_update_node(dev, node, info); } sm_media_session_schedule_rescan(dev->impl->session); } static const struct spa_device_events v4l2_device_events = { SPA_VERSION_DEVICE_EVENTS, .info = v4l2_device_info, .object_info = v4l2_device_object_info }; static struct device *v4l2_find_device(struct impl *impl, uint32_t id, const char *name) { struct device *dev; const char *str; spa_list_for_each(dev, &impl->device_list, link) { if (dev->id == id) return dev; if (name != NULL && (str = pw_properties_get(dev->props, PW_KEY_DEVICE_NAME)) != NULL && spa_streq(str, name)) return dev; } return NULL; } static void v4l2_update_device(struct impl *impl, struct device *dev, const struct spa_device_object_info *info) { pw_log_debug("update device %u", dev->id); if (pw_log_level_enabled(SPA_LOG_LEVEL_DEBUG)) spa_debug_dict(0, info->props); pw_properties_update(dev->props, info->props); } static int v4l2_update_device_props(struct device *dev) { struct pw_properties *p = dev->props; const char *s, *d; char temp[32], tmp[1024]; int i; if ((s = pw_properties_get(p, SPA_KEY_DEVICE_NAME)) == NULL) { if ((s = pw_properties_get(p, SPA_KEY_DEVICE_BUS_ID)) == NULL) { if ((s = pw_properties_get(p, SPA_KEY_DEVICE_BUS_PATH)) == NULL) { snprintf(temp, sizeof(temp), "%d", dev->id); s = temp; } } } pw_properties_set(p, PW_KEY_DEVICE_NAME, sm_media_session_sanitize_name(tmp, sizeof(tmp), '_', "v4l2_device.%s", s)); for (i = 2; i <= 99; i++) { if ((d = pw_properties_get(p, PW_KEY_DEVICE_NAME)) == NULL) break; if (v4l2_find_device(dev->impl, SPA_ID_INVALID, d) == NULL) break; pw_properties_set(p, PW_KEY_DEVICE_NAME, sm_media_session_sanitize_name(tmp, sizeof(tmp), '_', "v4l2_device.%s.%d", s, i)); } if (i == 99) return -EEXIST; if (pw_properties_get(p, PW_KEY_DEVICE_DESCRIPTION) == NULL) { d = pw_properties_get(p, PW_KEY_DEVICE_PRODUCT_NAME); if (!d) d = "Unknown device"; pw_properties_set(p, PW_KEY_DEVICE_DESCRIPTION, d); } return 0; } static void set_profile(struct device *device, int index) { char buf[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); pw_log_debug("%p: set profile %d id:%d", device, index, device->device_id); device->profile = index; if (device->device_id != 0) { spa_device_set_param(device->device, SPA_PARAM_Profile, 0, spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile, SPA_PARAM_PROFILE_index, SPA_POD_Int(index))); } } static void device_destroy(void *data) { struct device *device = data; struct node *node; pw_log_debug("device %p destroy", device); spa_list_remove(&device->link); spa_list_consume(node, &device->node_list, link) v4l2_remove_node(device, node); if (device->appeared) spa_hook_remove(&device->device_listener); } static void device_free(void *data) { struct device *device = data; pw_log_debug("device %p free", device); spa_hook_remove(&device->listener); pw_unload_spa_handle(device->handle); pw_properties_free(device->props); sm_object_discard(&device->sdevice->obj); free(device); } static void device_update(void *data) { struct device *device = data; pw_log_debug("device %p appeared %d %d", device, device->appeared, device->profile); if (device->appeared) return; device->device_id = device->sdevice->obj.id; device->appeared = true; spa_device_add_listener(device->device, &device->device_listener, &v4l2_device_events, device); set_profile(device, 1); sm_object_sync_update(&device->sdevice->obj); } static const struct sm_object_events device_events = { SM_VERSION_OBJECT_EVENTS, .destroy = device_destroy, .free = device_free, .update = device_update, }; static struct device *v4l2_create_device(struct impl *impl, uint32_t id, const struct spa_device_object_info *info) { struct pw_context *context = impl->session->context; struct device *dev; struct spa_handle *handle; int res; void *iface; const char *rules; pw_log_debug("new device %u", id); if (!spa_streq(info->type, SPA_TYPE_INTERFACE_Device)) { errno = EINVAL; return NULL; } dev = calloc(1, sizeof(*dev)); if (dev == NULL) { res = -errno; goto exit; } dev->impl = impl; dev->id = id; dev->props = pw_properties_new_dict(info->props); v4l2_update_device_props(dev); if ((rules = pw_properties_get(impl->conf, "rules")) != NULL) sm_media_session_match_rules(rules, strlen(rules), dev->props); handle = pw_context_load_spa_handle(context, info->factory_name, &dev->props->dict); if (handle == NULL) { res = -errno; pw_log_error("can't make factory instance: %m"); goto clean_device; } if ((res = spa_handle_get_interface(handle, info->type, &iface)) < 0) { pw_log_error("can't get %s interface: %s", info->type, spa_strerror(res)); goto unload_handle; } dev->handle = handle; dev->device = iface; dev->sdevice = sm_media_session_export_device(impl->session, &dev->props->dict, dev->device); if (dev->sdevice == NULL) { res = -errno; goto clean_device; } pw_log_debug("got object %p", &dev->sdevice->obj); sm_object_add_listener(&dev->sdevice->obj, &dev->listener, &device_events, dev); spa_list_init(&dev->node_list); spa_list_append(&impl->device_list, &dev->link); return dev; unload_handle: pw_unload_spa_handle(handle); clean_device: pw_properties_free(dev->props); free(dev); exit: errno = -res; return NULL; } static void v4l2_remove_device(struct impl *impl, struct device *dev) { pw_log_debug("remove device %u", dev->id); if (dev->sdevice) sm_object_destroy(&dev->sdevice->obj); } static void v4l2_udev_object_info(void *data, uint32_t id, const struct spa_device_object_info *info) { struct impl *impl = data; struct device *dev; dev = v4l2_find_device(impl, id, NULL); if (info == NULL) { if (dev == NULL) return; v4l2_remove_device(impl, dev); } else if (dev == NULL) { if (v4l2_create_device(impl, id, info) == NULL) return; } else { v4l2_update_device(impl, dev, info); } } static const struct spa_device_events v4l2_udev_callbacks = { SPA_VERSION_DEVICE_EVENTS, .object_info = v4l2_udev_object_info, }; static void session_destroy(void *data) { struct impl *impl = data; spa_hook_remove(&impl->session_listener); spa_hook_remove(&impl->listener); pw_unload_spa_handle(impl->handle); pw_properties_free(impl->conf); free(impl); } static const struct sm_media_session_events session_events = { SM_VERSION_MEDIA_SESSION_EVENTS, .destroy = session_destroy, }; int sm_v4l2_monitor_start(struct sm_media_session *sess) { struct pw_context *context = sess->context; struct impl *impl; int res; void *iface; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; impl->conf = pw_properties_new(NULL, NULL); if (impl->conf == NULL) { res = -errno; goto out_free; } impl->session = sess; impl->handle = pw_context_load_spa_handle(context, SPA_NAME_API_V4L2_ENUM_UDEV, NULL); if (impl->handle == NULL) { res = -errno; pw_log_info("can't load %s: %m", SPA_NAME_API_V4L2_ENUM_UDEV); goto out_free; } if ((res = spa_handle_get_interface(impl->handle, SPA_TYPE_INTERFACE_Device, &iface)) < 0) { pw_log_error("can't get MONITOR interface: %d", res); goto out_unload; } impl->monitor = iface; spa_list_init(&impl->device_list); if ((res = sm_media_session_load_conf(impl->session, SESSION_CONF, impl->conf)) < 0) pw_log_info("can't load "SESSION_CONF" config: %s", spa_strerror(res)); spa_device_add_listener(impl->monitor, &impl->listener, &v4l2_udev_callbacks, impl); sm_media_session_add_listener(sess, &impl->session_listener, &session_events, impl); return 0; out_unload: pw_unload_spa_handle(impl->handle); out_free: free(impl); return res; } 0707010000007B000041ED0000000000000000000000046178A88C00000000000000000000000000000000000000000000001C00000000media-session-0.4.1/systemd0707010000007C000081A40000000000000000000000016178A88C0000009C000000000000000000000000000000000000002800000000media-session-0.4.1/systemd/meson.buildif not get_option('systemd-system-service').disabled() subdir('system') endif if not get_option('systemd-user-service').disabled() subdir('user') endif 0707010000007D000041ED0000000000000000000000026178A88C00000000000000000000000000000000000000000000002300000000media-session-0.4.1/systemd/system0707010000007E000081A40000000000000000000000016178A88C0000025A000000000000000000000000000000000000002F00000000media-session-0.4.1/systemd/system/meson.buildsystemd_system_services_dir = systemd.get_variable(pkgconfig: 'systemdsystemunitdir', pkgconfig_define: [ 'prefix', prefix]) systemd_config = configuration_data() systemd_config.set('PW_MEDIA_SESSION_BINARY', media_session_bindir / 'pipewire-media-session') if get_option('session-managers').contains('media-session') configure_file(input : 'pipewire-media-session.service.in', output : 'pipewire-media-session.service', configuration : systemd_config, install_dir : systemd_system_services_dir) endif 0707010000007F000081A40000000000000000000000016178A88C000001D1000000000000000000000000000000000000004500000000media-session-0.4.1/systemd/system/pipewire-media-session.service.in[Unit] Description=PipeWire Media Session Manager After=pipewire.service BindsTo=pipewire.service [Service] LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=yes RestrictNamespaces=yes SystemCallArchitectures=native SystemCallFilter=@system-service Type=simple ExecStart=@PW_MEDIA_SESSION_BINARY@ Restart=on-failure User=pipewire Environment=PIPEWIRE_RUNTIME_DIR=%t/pipewire [Install] WantedBy=pipewire.service Alias=pipewire-session-manager.service 07070100000080000041ED0000000000000000000000026178A88C00000000000000000000000000000000000000000000002100000000media-session-0.4.1/systemd/user07070100000081000081A40000000000000000000000016178A88C00000280000000000000000000000000000000000000002D00000000media-session-0.4.1/systemd/user/meson.buildsystemd_user_services_dir = systemd.get_variable(pkgconfig: 'systemduserunitdir', pkgconfig_define: [ 'prefix', prefix]) if get_option('systemd-user-unit-dir') != '' systemd_user_services_dir = get_option('systemd-user-unit-dir') endif systemd_config = configuration_data() systemd_config.set('PW_MEDIA_SESSION_BINARY', media_session_bindir / 'pipewire-media-session') configure_file(input : 'pipewire-media-session.service.in', output : 'pipewire-media-session.service', configuration : systemd_config, install_dir : systemd_user_services_dir) 07070100000082000081A40000000000000000000000016178A88C000001AA000000000000000000000000000000000000004300000000media-session-0.4.1/systemd/user/pipewire-media-session.service.in[Unit] Description=PipeWire Media Session Manager After=pipewire.service BindsTo=pipewire.service [Service] LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=yes RestrictNamespaces=yes SystemCallArchitectures=native SystemCallFilter=@system-service Type=simple ExecStart=@PW_MEDIA_SESSION_BINARY@ Restart=on-failure Slice=session.slice [Install] WantedBy=pipewire.service Alias=pipewire-session-manager.service 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!1214 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor