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:
parent
bd8ec29bb5
commit
1bced6b2ef
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
*/
|
|
@ -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' ]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
@ -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 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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
|
||||
]
|
||||
}
|
|
@ -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')
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
|
||||
subdir('pipewire')
|
||||
subdir('media-session')
|
||||
subdir('daemon')
|
||||
subdir('tools')
|
||||
subdir('modules')
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
#include "pipewire/pipewire.h"
|
||||
|
||||
#include "../media-session/reserve.c"
|
||||
#include "reserve.c"
|
||||
|
||||
struct impl {
|
||||
struct pw_main_loop *mainloop;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
[wrap-git]
|
||||
url = https://gitlab.freedesktop.org/pipewire/media-session.git
|
||||
revision = head
|
||||
|
Loading…
Reference in New Issue