mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-11-08 13:30:08 -05:00
config.h needs to be consistently included before any standard headers if we ever want to set feature test macros (like _GNU_SOURCE or whatever) inside. It can lead to hard-to-debug issues without that. It can also be problematic just for our own HAVE_* that it may define if it's not consistently made available before our own headers. Just always include it first, before everything. We already did this in many files, just not consistently.
462 lines
10 KiB
C
462 lines
10 KiB
C
/* PipeWire */
|
|
/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
|
|
/* SPDX-License-Identifier: MIT */
|
|
|
|
#include "config.h"
|
|
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <errno.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
|
|
#include <spa/utils/result.h>
|
|
#include <spa/utils/string.h>
|
|
#include <spa/param/audio/raw.h>
|
|
|
|
#include <pipewire/impl.h>
|
|
#include <pipewire/i18n.h>
|
|
|
|
/** \page page_module_fallback_sink Fallback Sink
|
|
*
|
|
* Fallback sink, which appear dynamically when no other sinks are
|
|
* present. This is only useful for Pulseaudio compatibility.
|
|
*
|
|
* ## Module Name
|
|
*
|
|
* `libpipewire-module-fallback-sink`
|
|
*
|
|
* ## Module Options
|
|
*
|
|
* - `sink.name`: sink name
|
|
* - `sink.description`: sink description
|
|
*/
|
|
|
|
#define NAME "fallback-sink"
|
|
|
|
#define DEFAULT_SINK_NAME "auto_null"
|
|
#define DEFAULT_SINK_DESCRIPTION _("Dummy Output")
|
|
|
|
PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
|
|
#define PW_LOG_TOPIC_DEFAULT mod_topic
|
|
|
|
#define MODULE_USAGE ("( sink.name=<str> ) " \
|
|
"( sink.description=<str> ) ")
|
|
|
|
static const struct spa_dict_item module_props[] = {
|
|
{ PW_KEY_MODULE_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
|
|
{ PW_KEY_MODULE_DESCRIPTION, "Dynamically appearing fallback sink" },
|
|
{ PW_KEY_MODULE_USAGE, MODULE_USAGE },
|
|
{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
|
|
};
|
|
|
|
struct bitmap {
|
|
uint8_t *data;
|
|
size_t size;
|
|
size_t items;
|
|
};
|
|
|
|
struct impl {
|
|
struct pw_context *context;
|
|
|
|
struct pw_impl_module *module;
|
|
struct spa_hook module_listener;
|
|
|
|
struct pw_core *core;
|
|
struct pw_registry *registry;
|
|
struct pw_proxy *sink;
|
|
|
|
struct spa_hook core_listener;
|
|
struct spa_hook core_proxy_listener;
|
|
struct spa_hook registry_listener;
|
|
struct spa_hook sink_listener;
|
|
|
|
struct pw_properties *properties;
|
|
|
|
struct bitmap sink_ids;
|
|
struct bitmap fallback_sink_ids;
|
|
|
|
int check_seq;
|
|
|
|
unsigned int do_disconnect:1;
|
|
unsigned int scheduled:1;
|
|
};
|
|
|
|
static int bitmap_add(struct bitmap *map, uint32_t i)
|
|
{
|
|
const uint32_t pos = (i >> 3);
|
|
const uint8_t mask = 1 << (i & 0x7);
|
|
|
|
if (pos >= map->size) {
|
|
size_t new_size = map->size + pos + 16;
|
|
void *p;
|
|
|
|
p = realloc(map->data, new_size);
|
|
if (!p)
|
|
return -errno;
|
|
|
|
memset((uint8_t*)p + map->size, 0, new_size - map->size);
|
|
map->data = p;
|
|
map->size = new_size;
|
|
}
|
|
|
|
if (map->data[pos] & mask)
|
|
return 1;
|
|
|
|
map->data[pos] |= mask;
|
|
++map->items;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool bitmap_remove(struct bitmap *map, uint32_t i)
|
|
{
|
|
const uint32_t pos = (i >> 3);
|
|
const uint8_t mask = 1 << (i & 0x7);
|
|
|
|
if (pos >= map->size)
|
|
return false;
|
|
|
|
if (!(map->data[pos] & mask))
|
|
return false;
|
|
|
|
map->data[pos] &= ~mask;
|
|
--map->items;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void bitmap_free(struct bitmap *map)
|
|
{
|
|
free(map->data);
|
|
spa_zero(*map);
|
|
}
|
|
|
|
static int add_id(struct bitmap *map, uint32_t id)
|
|
{
|
|
int res;
|
|
|
|
if (id == SPA_ID_INVALID)
|
|
return -EINVAL;
|
|
|
|
if ((res = bitmap_add(map, id)) < 0)
|
|
pw_log_error("%s", spa_strerror(res));
|
|
|
|
return res;
|
|
}
|
|
|
|
static void reschedule_check(struct impl *impl)
|
|
{
|
|
if (!impl->scheduled)
|
|
return;
|
|
|
|
impl->check_seq = pw_core_sync(impl->core, 0, impl->check_seq);
|
|
}
|
|
|
|
static void schedule_check(struct impl *impl)
|
|
{
|
|
if (impl->scheduled)
|
|
return;
|
|
|
|
impl->scheduled = true;
|
|
impl->check_seq = pw_core_sync(impl->core, 0, impl->check_seq);
|
|
}
|
|
|
|
static void sink_proxy_removed(void *data)
|
|
{
|
|
struct impl *impl = data;
|
|
|
|
pw_proxy_destroy(impl->sink);
|
|
}
|
|
|
|
static void sink_proxy_bound_props(void *data, uint32_t id, const struct spa_dict *props)
|
|
{
|
|
struct impl *impl = data;
|
|
|
|
add_id(&impl->sink_ids, id);
|
|
add_id(&impl->fallback_sink_ids, id);
|
|
|
|
reschedule_check(impl);
|
|
schedule_check(impl);
|
|
}
|
|
|
|
static void sink_proxy_destroy(void *data)
|
|
{
|
|
struct impl *impl = data;
|
|
|
|
pw_log_debug("fallback dummy sink destroyed");
|
|
|
|
spa_hook_remove(&impl->sink_listener);
|
|
impl->sink = NULL;
|
|
}
|
|
|
|
static const struct pw_proxy_events sink_proxy_events = {
|
|
PW_VERSION_PROXY_EVENTS,
|
|
.removed = sink_proxy_removed,
|
|
.bound_props = sink_proxy_bound_props,
|
|
.destroy = sink_proxy_destroy,
|
|
};
|
|
|
|
static int sink_create(struct impl *impl)
|
|
{
|
|
if (impl->sink)
|
|
return 0;
|
|
|
|
pw_log_info("creating fallback dummy sink");
|
|
|
|
impl->sink = pw_core_create_object(impl->core,
|
|
"adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE,
|
|
impl->properties ? &impl->properties->dict : NULL, 0);
|
|
if (impl->sink == NULL)
|
|
return -errno;
|
|
|
|
pw_proxy_add_listener(impl->sink, &impl->sink_listener, &sink_proxy_events, impl);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void sink_destroy(struct impl *impl)
|
|
{
|
|
if (!impl->sink)
|
|
return;
|
|
|
|
pw_log_info("removing fallback dummy sink");
|
|
pw_proxy_destroy(impl->sink);
|
|
}
|
|
|
|
static void check_sinks(struct impl *impl)
|
|
{
|
|
int res;
|
|
|
|
pw_log_debug("seeing %zu sink(s), %zu fallback sink(s)",
|
|
impl->sink_ids.items, impl->fallback_sink_ids.items);
|
|
|
|
if (impl->sink_ids.items > impl->fallback_sink_ids.items) {
|
|
sink_destroy(impl);
|
|
} else {
|
|
if ((res = sink_create(impl)) < 0)
|
|
pw_log_error("error creating sink: %s", spa_strerror(res));
|
|
}
|
|
}
|
|
|
|
static void registry_event_global(void *data, uint32_t id,
|
|
uint32_t permissions, const char *type, uint32_t version,
|
|
const struct spa_dict *props)
|
|
{
|
|
struct impl *impl = data;
|
|
const char *str;
|
|
|
|
reschedule_check(impl);
|
|
|
|
if (!props)
|
|
return;
|
|
|
|
if (!spa_streq(type, PW_TYPE_INTERFACE_Node))
|
|
return;
|
|
|
|
str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
|
|
if (!(spa_streq(str, "Audio/Sink") || spa_streq(str, "Audio/Sink/Virtual")))
|
|
return;
|
|
|
|
add_id(&impl->sink_ids, id);
|
|
schedule_check(impl);
|
|
}
|
|
|
|
static void registry_event_global_remove(void *data, uint32_t id)
|
|
{
|
|
struct impl *impl = data;
|
|
|
|
reschedule_check(impl);
|
|
|
|
bitmap_remove(&impl->fallback_sink_ids, id);
|
|
if (bitmap_remove(&impl->sink_ids, id))
|
|
schedule_check(impl);
|
|
}
|
|
|
|
static const struct pw_registry_events registry_events = {
|
|
PW_VERSION_REGISTRY_EVENTS,
|
|
.global = registry_event_global,
|
|
.global_remove = registry_event_global_remove,
|
|
};
|
|
|
|
static void core_done(void *data, uint32_t id, int seq)
|
|
{
|
|
struct impl *impl = data;
|
|
|
|
if (seq == impl->check_seq) {
|
|
impl->scheduled = false;
|
|
check_sinks(impl);
|
|
}
|
|
}
|
|
|
|
static const struct pw_core_events core_events = {
|
|
PW_VERSION_CORE_EVENTS,
|
|
.done = core_done,
|
|
};
|
|
|
|
static void core_proxy_removed(void *data)
|
|
{
|
|
struct impl *impl = data;
|
|
|
|
if (impl->registry) {
|
|
spa_hook_remove(&impl->registry_listener);
|
|
pw_proxy_destroy((struct pw_proxy*)impl->registry);
|
|
impl->registry = NULL;
|
|
}
|
|
}
|
|
|
|
static void core_proxy_destroy(void *data)
|
|
{
|
|
struct impl *impl = data;
|
|
|
|
spa_hook_remove(&impl->core_listener);
|
|
spa_hook_remove(&impl->core_proxy_listener);
|
|
impl->core = NULL;
|
|
}
|
|
|
|
static const struct pw_proxy_events core_proxy_events = {
|
|
PW_VERSION_PROXY_EVENTS,
|
|
.destroy = core_proxy_destroy,
|
|
.removed = core_proxy_removed,
|
|
};
|
|
|
|
static void impl_destroy(struct impl *impl)
|
|
{
|
|
sink_destroy(impl);
|
|
|
|
if (impl->registry) {
|
|
spa_hook_remove(&impl->registry_listener);
|
|
pw_proxy_destroy((struct pw_proxy*)impl->registry);
|
|
impl->registry = NULL;
|
|
}
|
|
|
|
if (impl->core) {
|
|
spa_hook_remove(&impl->core_listener);
|
|
spa_hook_remove(&impl->core_proxy_listener);
|
|
if (impl->do_disconnect)
|
|
pw_core_disconnect(impl->core);
|
|
impl->core = NULL;
|
|
}
|
|
|
|
if (impl->properties) {
|
|
pw_properties_free(impl->properties);
|
|
impl->properties = NULL;
|
|
}
|
|
|
|
bitmap_free(&impl->sink_ids);
|
|
bitmap_free(&impl->fallback_sink_ids);
|
|
|
|
free(impl);
|
|
}
|
|
|
|
static void module_destroy(void *data)
|
|
{
|
|
struct impl *impl = data;
|
|
spa_hook_remove(&impl->module_listener);
|
|
impl_destroy(impl);
|
|
}
|
|
|
|
static const struct pw_impl_module_events module_events = {
|
|
PW_VERSION_IMPL_MODULE_EVENTS,
|
|
.destroy = module_destroy,
|
|
};
|
|
|
|
SPA_EXPORT
|
|
int pipewire__module_init(struct pw_impl_module *module, const char *args)
|
|
{
|
|
struct pw_context *context = pw_impl_module_get_context(module);
|
|
struct pw_properties *props = NULL;
|
|
struct impl *impl = NULL;
|
|
const char *str;
|
|
int res;
|
|
|
|
PW_LOG_TOPIC_INIT(mod_topic);
|
|
|
|
impl = calloc(1, sizeof(struct impl));
|
|
if (impl == NULL)
|
|
goto error_errno;
|
|
|
|
pw_log_debug("module %p: new %s", impl, args);
|
|
|
|
if (args == NULL)
|
|
args = "";
|
|
|
|
impl->module = module;
|
|
impl->context = context;
|
|
|
|
props = pw_properties_new_string(args);
|
|
if (props == NULL)
|
|
goto error_errno;
|
|
|
|
impl->properties = pw_properties_new(NULL, NULL);
|
|
if (impl->properties == NULL)
|
|
goto error_errno;
|
|
|
|
if ((str = pw_properties_get(props, "sink.name")) == NULL)
|
|
str = DEFAULT_SINK_NAME;
|
|
pw_properties_set(impl->properties, PW_KEY_NODE_NAME, str);
|
|
|
|
if ((str = pw_properties_get(props, "sink.description")) == NULL)
|
|
str = DEFAULT_SINK_DESCRIPTION;
|
|
pw_properties_set(impl->properties, PW_KEY_NODE_DESCRIPTION, str);
|
|
|
|
pw_properties_setf(impl->properties, SPA_KEY_AUDIO_RATE, "%u", 48000);
|
|
pw_properties_setf(impl->properties, SPA_KEY_AUDIO_CHANNELS, "%u", 2);
|
|
pw_properties_set(impl->properties, SPA_KEY_AUDIO_POSITION, "FL,FR");
|
|
|
|
pw_properties_set(impl->properties, PW_KEY_MEDIA_CLASS, "Audio/Sink");
|
|
pw_properties_set(impl->properties, PW_KEY_FACTORY_NAME, "support.null-audio-sink");
|
|
pw_properties_set(impl->properties, PW_KEY_NODE_VIRTUAL, "true");
|
|
pw_properties_set(impl->properties, "monitor.channel-volumes", "true");
|
|
|
|
impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
|
|
if (impl->core == NULL) {
|
|
str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
|
|
impl->core = pw_context_connect(impl->context,
|
|
pw_properties_new(
|
|
PW_KEY_REMOTE_NAME, str,
|
|
NULL),
|
|
0);
|
|
impl->do_disconnect = true;
|
|
}
|
|
if (impl->core == NULL) {
|
|
res = -errno;
|
|
pw_log_error("can't connect: %m");
|
|
goto error;
|
|
}
|
|
|
|
pw_proxy_add_listener((struct pw_proxy*)impl->core,
|
|
&impl->core_proxy_listener, &core_proxy_events,
|
|
impl);
|
|
|
|
pw_core_add_listener(impl->core, &impl->core_listener, &core_events, impl);
|
|
|
|
impl->registry = pw_core_get_registry(impl->core,
|
|
PW_VERSION_REGISTRY, 0);
|
|
if (impl->registry == NULL)
|
|
goto error_errno;
|
|
|
|
pw_registry_add_listener(impl->registry,
|
|
&impl->registry_listener,
|
|
®istry_events, impl);
|
|
|
|
pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
|
|
|
|
pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
|
|
|
|
schedule_check(impl);
|
|
|
|
pw_properties_free(props);
|
|
return 0;
|
|
|
|
error_errno:
|
|
res = -errno;
|
|
error:
|
|
if (props)
|
|
pw_properties_free(props);
|
|
if (impl)
|
|
impl_destroy(impl);
|
|
return res;
|
|
}
|