Remove media-session from this tree

It is now available as a separate project in
https://gitlab.freedesktop.org/pipewire/media-session

The code required by pw-reservice has moved to src/tools/reserve.{c|h}
This commit is contained in:
Peter Hutterer 2021-10-14 10:24:21 +10:00
parent bd8ec29bb5
commit 1bced6b2ef
52 changed files with 21 additions and 16470 deletions

View file

@ -317,13 +317,6 @@ doccheck:
- .build_on_fedora - .build_on_fedora
stage: analysis stage: analysis
script: 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
# Check that each pipewire module has a \subpage entry # Check that each pipewire module has a \subpage entry
- git grep -h -o -e "\\\page page_module_\w\+" | cut -f2 -d' ' > pipewire_module_pages - git grep -h -o -e "\\\page page_module_\w\+" | cut -f2 -d' ' > pipewire_module_pages
- cat pipewire_module_pages - cat pipewire_module_pages

View file

@ -1,67 +0,0 @@
/**
\page page_media_session Media Session
PipeWire Media Session is the reference/example session manager provided by
the PipeWire 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
*/

View file

@ -30,7 +30,6 @@ extra_docs = [
'pipewire-session-manager.dox', 'pipewire-session-manager.dox',
'pipewire-objects-design.dox', 'pipewire-objects-design.dox',
'pipewire-audio.dox', 'pipewire-audio.dox',
'media-session.dox',
'tutorial.dox', 'tutorial.dox',
'tutorial1.dox', 'tutorial1.dox',
'tutorial2.dox', 'tutorial2.dox',
@ -67,9 +66,6 @@ endforeach
foreach h : module_sources foreach h : module_sources
inputs += meson.source_root() / 'src' / 'modules' / h inputs += meson.source_root() / 'src' / 'modules' / h
endforeach endforeach
foreach h : media_session_sources
inputs += meson.source_root() / 'src' / 'media-session' / h
endforeach
inputs += meson.source_root() / 'test' / 'pwtest.h' inputs += meson.source_root() / 'test' / 'pwtest.h'
input_dirs = [ meson.source_root() / 'spa' / 'include' / 'spa' ] input_dirs = [ meson.source_root() / 'spa' / 'include' / 'spa' ]

View file

@ -10,7 +10,8 @@ consistent of Devices, Nodes and Ports. The session manager is the one that
decides on the links between those elements. decides on the links between those elements.
Two prominent session managers currently exist: Two prominent session managers currently exist:
- \ref page_media_session - the reference session manager provided by PipeWire - [PipeWire Media Session](https://gitlab.freedesktop.org/pipewire/pipewire-media-session), the
example session manager
- [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber), a - [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber), a
modular session manager based on GObject modular session manager based on GObject

View file

@ -491,8 +491,6 @@ if meson.version().version_compare('>=0.58.0')
devenv.set('PIPEWIRE_CONFIG_DIR', builddir / 'src' / 'daemon') devenv.set('PIPEWIRE_CONFIG_DIR', builddir / 'src' / 'daemon')
devenv.set('PIPEWIRE_MODULE_DIR', builddir / 'src' / 'modules') devenv.set('PIPEWIRE_MODULE_DIR', builddir / 'src' / 'modules')
devenv.set('MEDIA_SESSION_CONFIG_DIR', builddir / 'src' / 'media-session' / 'media-session.d')
devenv.set('SPA_PLUGIN_DIR', builddir / 'spa' / 'plugins') devenv.set('SPA_PLUGIN_DIR', builddir / 'spa' / 'plugins')
devenv.set('SPA_DATA_DIR', srcdir / 'spa' / 'plugins') devenv.set('SPA_DATA_DIR', srcdir / 'spa' / 'plugins')

View file

@ -36,7 +36,6 @@ fi
# the config file read by the daemon # the config file read by the daemon
export PIPEWIRE_CONFIG_DIR="${BUILDDIR}/src/daemon" export PIPEWIRE_CONFIG_DIR="${BUILDDIR}/src/daemon"
export MEDIA_SESSION_CONFIG_DIR="${BUILDDIR}/src/media-session/media-session.d"
# the directory with SPA plugins # the directory with SPA plugins
export SPA_PLUGIN_DIR="${BUILDDIR}/spa/plugins" export SPA_PLUGIN_DIR="${BUILDDIR}/spa/plugins"
export SPA_DATA_DIR="${SCRIPT_DIR}/spa/plugins" export SPA_DATA_DIR="${SCRIPT_DIR}/spa/plugins"
@ -57,6 +56,9 @@ export PKG_CONFIG_PATH="${BUILDDIR}/meson-uninstalled/:${PKG_CONFIG_PATH}"
if [ -d "${BUILDDIR}/subprojects/wireplumber" ]; then if [ -d "${BUILDDIR}/subprojects/wireplumber" ]; then
# FIXME: find a nice, shell-neutral way to specify a prompt # FIXME: find a nice, shell-neutral way to specify a prompt
"${SCRIPT_DIR}"/subprojects/wireplumber/wp-uninstalled.sh -b"${BUILDDIR}"/subprojects/wireplumber "${SHELL}" "${SCRIPT_DIR}"/subprojects/wireplumber/wp-uninstalled.sh -b"${BUILDDIR}"/subprojects/wireplumber "${SHELL}"
elif [ -d "${BUILDDIR}/subprojects/media-session" ]; then
# FIXME: find a nice, shell-neutral way to specify a prompt
"${SCRIPT_DIR}"/subprojects/media-session/media-session-uninstalled.sh -b"${BUILDDIR}"/subprojects/media-session "${SHELL}"
else else
# FIXME: find a nice, shell-neutral way to specify a prompt # FIXME: find a nice, shell-neutral way to specify a prompt
${SHELL} ${SHELL}

View file

@ -36,11 +36,20 @@ summary({'Build media-session': build_ms,
if build_wp if build_wp
wp_proj = subproject('wireplumber', required : true) wp_proj = subproject('wireplumber', required : true)
endif endif
if build_ms
ms_proj = subproject('media-session', required : true)
endif
if default_sm == '' if default_sm == ''
summary({'No session manager': 'pw-uninstalled.sh will not work out of the box!'}) summary({'No session manager': 'pw-uninstalled.sh will not work out of the box!'})
elif default_sm == 'media-session' elif default_sm == 'media-session'
conf_config_uninstalled.set('session_manager_path', pipewire_media_session.full_path()) ms_bindir = ms_proj.get_variable('media_session_bin_dir', pipewire_bindir)
conf_config.set('session_manager_path', ms_bindir / 'pipewire-media-session')
conf_config_uninstalled.set('session_manager_path',
meson.source_root() / 'subprojects' / 'pipewire-media-session' / 'media-session-uninstalled.sh')
conf_config_uninstalled.set('session_manager_args',
'-b ' + meson.build_root() / 'subprojects' / 'pipewire-media-session' + ' pipewire-media-session')
conf_config_uninstalled.set('sm_comment', '') conf_config_uninstalled.set('sm_comment', '')
elif default_sm == 'wireplumber' elif default_sm == 'wireplumber'
wp_bindir = wp_proj.get_variable('wireplumber_bin_dir', pipewire_bindir) wp_bindir = wp_proj.get_variable('wireplumber_bin_dir', pipewire_bindir)

View file

@ -5,16 +5,8 @@ install_data(sources : 'pipewire.socket',
systemd_config = configuration_data() systemd_config = configuration_data()
systemd_config.set('PW_BINARY', pipewire_bindir / 'pipewire') systemd_config.set('PW_BINARY', pipewire_bindir / 'pipewire')
systemd_config.set('PW_MEDIA_SESSION_BINARY', pipewire_bindir / 'pipewire-media-session')
configure_file(input : 'pipewire.service.in', configure_file(input : 'pipewire.service.in',
output : 'pipewire.service', output : 'pipewire.service',
configuration : systemd_config, configuration : systemd_config,
install_dir : systemd_system_services_dir) install_dir : systemd_system_services_dir)
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

View file

@ -1,21 +0,0 @@
[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

View file

@ -10,7 +10,6 @@ install_data(
systemd_config = configuration_data() systemd_config = configuration_data()
systemd_config.set('PW_BINARY', pipewire_bindir / 'pipewire') systemd_config.set('PW_BINARY', pipewire_bindir / 'pipewire')
systemd_config.set('PW_PULSE_BINARY', pipewire_bindir / 'pipewire-pulse') systemd_config.set('PW_PULSE_BINARY', pipewire_bindir / 'pipewire-pulse')
systemd_config.set('PW_MEDIA_SESSION_BINARY', pipewire_bindir / 'pipewire-media-session')
configure_file(input : 'pipewire.service.in', configure_file(input : 'pipewire.service.in',
output : 'pipewire.service', output : 'pipewire.service',
@ -21,10 +20,3 @@ configure_file(input : 'pipewire-pulse.service.in',
output : 'pipewire-pulse.service', output : 'pipewire-pulse.service',
configuration : systemd_config, configuration : systemd_config,
install_dir : systemd_user_services_dir) install_dir : systemd_user_services_dir)
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_user_services_dir)
endif

View file

@ -1,20 +0,0 @@
[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

View file

@ -1,216 +0,0 @@
/* 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;
}

View file

@ -1,669 +0,0 @@
/* 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;
}

View file

@ -1,774 +0,0 @@
/* 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;
}

View file

@ -1,219 +0,0 @@
/* 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;
}

File diff suppressed because it is too large Load diff

View file

@ -1,49 +0,0 @@
/* 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;
}

View file

@ -1,701 +0,0 @@
/* 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;
}

View file

@ -1,702 +0,0 @@
/* 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;
}

View file

@ -1,766 +0,0 @@
/* 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;
}

View file

@ -1,207 +0,0 @@
/* 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;
}

View file

@ -1,514 +0,0 @@
/* 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;
}

View file

@ -1,990 +0,0 @@
/* 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;
}

View file

@ -1,511 +0,0 @@
/* 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;
}

View file

@ -1,143 +0,0 @@
/* 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;
}

View file

@ -1,145 +0,0 @@
/* 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;
}

File diff suppressed because it is too large Load diff

View file

@ -1,126 +0,0 @@
# ALSA monitor config file for PipeWire version @VERSION@ #
#
# Copy and edit this file in @PIPEWIRE_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 ]
}
}
}
]

View file

@ -1,139 +0,0 @@
# Bluez monitor config file for PipeWire version @VERSION@ #
#
# Copy and edit this file in @PIPEWIRE_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
}
}
}
]

View file

@ -1,117 +0,0 @@
# Media session config file for PipeWire version @VERSION@ #
#
# Copy and edit this file in @PIPEWIRE_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
]
}

View file

@ -1,25 +0,0 @@
conf_config = configuration_data()
conf_config.set('VERSION', '"@0@"'.format(pipewire_version))
conf_config.set('PIPEWIRE_CONFIG_DIR', pipewire_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' ],
[ 'with-jack', 'with-jack' ],
[ 'with-pulseaudio', 'with-pulseaudio' ],
]
foreach c : conf_files
configure_file(input : c.get(0),
output : c.get(1),
configuration : conf_config,
install_dir : pipewire_confdatadir / 'media-session.d')
endforeach
install_data(
sources : [
'with-jack',
'with-pulseaudio' ],
install_dir : pipewire_confdatadir / 'media-session.d')

View file

@ -1,50 +0,0 @@
# V4L2 monitor config file for PipeWire version @VERSION@ #
#
# Copy and edit this file in @PIPEWIRE_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
}
}
}
]

View file

@ -1,328 +0,0 @@
/* 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

View file

@ -1,42 +0,0 @@
media_session_sources = []
if get_option('session-managers').contains('media-session')
sm_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',
'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,
install: true,
dependencies : [dbus_dep, pipewire_dep, alsa_dep, mathlib, sm_logind_dep, libinotify_dep],
)
subdir('media-session.d')
endif

View file

@ -1,109 +0,0 @@
/* 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;
}

View file

@ -1,534 +0,0 @@
/* 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;
}

File diff suppressed because it is too large Load diff

View file

@ -1,593 +0,0 @@
/* 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;
}

View file

@ -1,178 +0,0 @@
/* 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;
}

View file

@ -1,619 +0,0 @@
/* 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;
}

View file

@ -1,49 +0,0 @@
/* 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;
}

View file

@ -1,269 +0,0 @@
/* 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;
}

View file

@ -1,651 +0,0 @@
/* 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;
}

View file

@ -1,591 +0,0 @@
/* 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;
}

View file

@ -1,6 +1,5 @@
subdir('pipewire') subdir('pipewire')
subdir('media-session')
subdir('daemon') subdir('daemon')
subdir('tools') subdir('tools')
subdir('modules') subdir('modules')

View file

@ -69,6 +69,7 @@ summary({'Build pw-cat tool': build_pw_cat}, bool_yn: true, section: 'pw-cat/pw-
if dbus_dep.found() if dbus_dep.found()
executable('pw-reserve', executable('pw-reserve',
'reserve.h',
'pw-reserve.c', 'pw-reserve.c',
install: true, install: true,
dependencies : [dbus_dep, pipewire_dep], dependencies : [dbus_dep, pipewire_dep],

View file

@ -33,7 +33,7 @@
#include "pipewire/pipewire.h" #include "pipewire/pipewire.h"
#include "../media-session/reserve.c" #include "reserve.c"
struct impl { struct impl {
struct pw_main_loop *mainloop; struct pw_main_loop *mainloop;

View file

@ -0,0 +1,4 @@
[wrap-git]
url = https://gitlab.freedesktop.org/pipewire/media-session.git
revision = head