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
stage: analysis
script:
# Check that each media session module has a \subpage entry
- git grep -h -o -e "\\\page page_media_session_module_\w\+" | cut -f2 -d' ' > media_session_pages
- cat media_session_pages
- |
for page in $(cat media_session_pages); do
git grep -q -e "\\\subpage $page" || (echo "\\page $page is missing \\subpage entry in doc/media-session.dox" && false)
done
# Check that each pipewire module has a \subpage entry
- git grep -h -o -e "\\\page page_module_\w\+" | cut -f2 -d' ' > 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-objects-design.dox',
'pipewire-audio.dox',
'media-session.dox',
'tutorial.dox',
'tutorial1.dox',
'tutorial2.dox',
@ -67,9 +66,6 @@ endforeach
foreach h : module_sources
inputs += meson.source_root() / 'src' / 'modules' / h
endforeach
foreach h : media_session_sources
inputs += meson.source_root() / 'src' / 'media-session' / h
endforeach
inputs += meson.source_root() / 'test' / 'pwtest.h'
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.
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
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_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_DATA_DIR', srcdir / 'spa' / 'plugins')

View File

@ -36,7 +36,6 @@ fi
# the config file read by the 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
export SPA_PLUGIN_DIR="${BUILDDIR}/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
# FIXME: find a nice, shell-neutral way to specify a prompt
"${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
# FIXME: find a nice, shell-neutral way to specify a prompt
${SHELL}

View File

@ -36,11 +36,20 @@ summary({'Build media-session': build_ms,
if build_wp
wp_proj = subproject('wireplumber', required : true)
endif
if build_ms
ms_proj = subproject('media-session', required : true)
endif
if default_sm == ''
summary({'No session manager': 'pw-uninstalled.sh will not work out of the box!'})
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', '')
elif default_sm == 'wireplumber'
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.set('PW_BINARY', pipewire_bindir / 'pipewire')
systemd_config.set('PW_MEDIA_SESSION_BINARY', pipewire_bindir / 'pipewire-media-session')
configure_file(input : 'pipewire.service.in',
output : 'pipewire.service',
configuration : systemd_config,
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.set('PW_BINARY', pipewire_bindir / 'pipewire')
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',
output : 'pipewire.service',
@ -21,10 +20,3 @@ configure_file(input : 'pipewire-pulse.service.in',
output : 'pipewire-pulse.service',
configuration : systemd_config,
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('media-session')
subdir('daemon')
subdir('tools')
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()
executable('pw-reserve',
'reserve.h',
'pw-reserve.c',
install: true,
dependencies : [dbus_dep, pipewire_dep],

View File

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

View File

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