pipewire/src/modules/module-example-source.c
Sam James 2cec77e7df *: unify config.h handling
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.
2025-05-30 10:24:13 +00:00

448 lines
11 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 <stdlib.h>
#include <signal.h>
#include <limits.h>
#include <math.h>
#include <spa/utils/result.h>
#include <spa/utils/string.h>
#include <spa/utils/json.h>
#include <spa/utils/ringbuffer.h>
#include <spa/debug/types.h>
#include <spa/pod/builder.h>
#include <spa/param/audio/format-utils.h>
#include <spa/param/audio/raw.h>
#include <spa/param/audio/raw-json.h>
#include <pipewire/impl.h>
#include <pipewire/i18n.h>
/** \page page_module_example_source Example Source
*
* The example source is a good starting point for writing a custom
* source. We refer to the source code for more information.
*
* ## Module Name
*
* `libpipewire-module-example-source`
*
* ## Module Options
*
* - `node.name`: a unique name for the stream
* - `node.description`: a human readable name for the stream
* - `stream.props = {}`: properties to be passed to the stream
*
* ## General options
*
* Options with well-known behavior.
*
* - \ref PW_KEY_REMOTE_NAME
* - \ref PW_KEY_AUDIO_FORMAT
* - \ref PW_KEY_AUDIO_RATE
* - \ref PW_KEY_AUDIO_CHANNELS
* - \ref SPA_KEY_AUDIO_POSITION
* - \ref PW_KEY_MEDIA_NAME
* - \ref PW_KEY_NODE_LATENCY
* - \ref PW_KEY_NODE_NAME
* - \ref PW_KEY_NODE_DESCRIPTION
* - \ref PW_KEY_NODE_GROUP
* - \ref PW_KEY_NODE_VIRTUAL
* - \ref PW_KEY_MEDIA_CLASS
*
* ## Example configuration
*
*\code{.unparsed}
* # ~/.config/pipewire/pipewire.conf.d/my-example-source.conf
*
* context.modules = [
* { name = libpipewire-module-example-source
* args = {
* node.name = "example_source"
* node.description = "My Example Source"
* stream.props = {
* audio.position = [ FL FR ]
* }
* }
* }
* ]
*\endcode
*/
#define NAME "example-source"
PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
#define PW_LOG_TOPIC_DEFAULT mod_topic
#define DEFAULT_FORMAT "S16"
#define DEFAULT_RATE 48000
#define DEFAULT_CHANNELS 2
#define DEFAULT_POSITION "[ FL FR ]"
#define MODULE_USAGE "( node.latency=<latency as fraction> ) " \
"( node.name=<name of the nodes> ) " \
"( node.description=<description of the nodes> ) " \
"( audio.format=<format, default:"DEFAULT_FORMAT"> ) " \
"( audio.rate=<sample rate, default: "SPA_STRINGIFY(DEFAULT_RATE)"> ) " \
"( audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS) "> ) " \
"( audio.position=<channel map, default:"DEFAULT_POSITION"> ) " \
"( stream.props=<properties> ) "
static const struct spa_dict_item module_props[] = {
{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
{ PW_KEY_MODULE_DESCRIPTION, "An example audio source" },
{ PW_KEY_MODULE_USAGE, MODULE_USAGE },
{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
};
struct impl {
struct pw_context *context;
struct pw_properties *props;
struct pw_impl_module *module;
struct spa_hook module_listener;
struct pw_core *core;
struct spa_hook core_proxy_listener;
struct spa_hook core_listener;
struct pw_properties *stream_props;
struct pw_stream *stream;
struct spa_hook stream_listener;
struct spa_audio_info_raw info;
uint32_t frame_size;
unsigned int do_disconnect:1;
unsigned int unloading:1;
};
static void stream_destroy(void *d)
{
struct impl *impl = d;
spa_hook_remove(&impl->stream_listener);
impl->stream = NULL;
}
static void stream_state_changed(void *d, enum pw_stream_state old,
enum pw_stream_state state, const char *error)
{
struct impl *impl = d;
switch (state) {
case PW_STREAM_STATE_ERROR:
case PW_STREAM_STATE_UNCONNECTED:
pw_impl_module_schedule_destroy(impl->module);
break;
case PW_STREAM_STATE_PAUSED:
case PW_STREAM_STATE_STREAMING:
break;
default:
break;
}
}
static void capture_stream_process(void *d)
{
struct impl *impl = d;
struct pw_buffer *buf;
struct spa_data *bd;
void *data;
uint32_t size;
if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
pw_log_debug("out of buffers: %m");
return;
}
bd = &buf->buffer->datas[0];
data = bd->data;
size = buf->requested ? buf->requested * impl->frame_size : bd->maxsize;
/* fill buffer contents here */
pw_log_info("fill buffer data %p with up to %u bytes", data, size);
bd->chunk->size = size;
bd->chunk->stride = impl->frame_size;
bd->chunk->offset = 0;
buf->size = size / impl->frame_size;
pw_stream_queue_buffer(impl->stream, buf);
}
static const struct pw_stream_events capture_stream_events = {
PW_VERSION_STREAM_EVENTS,
.destroy = stream_destroy,
.state_changed = stream_state_changed,
.process = capture_stream_process
};
static int create_stream(struct impl *impl)
{
int res;
uint32_t n_params;
const struct spa_pod *params[1];
uint8_t buffer[1024];
struct spa_pod_builder b;
impl->stream = pw_stream_new(impl->core, "example source", impl->stream_props);
impl->stream_props = NULL;
if (impl->stream == NULL)
return -errno;
pw_stream_add_listener(impl->stream,
&impl->stream_listener,
&capture_stream_events, impl);
n_params = 0;
spa_pod_builder_init(&b, buffer, sizeof(buffer));
params[n_params++] = spa_format_audio_raw_build(&b,
SPA_PARAM_EnumFormat, &impl->info);
if ((res = pw_stream_connect(impl->stream,
PW_DIRECTION_OUTPUT,
PW_ID_ANY,
PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS |
PW_STREAM_FLAG_RT_PROCESS,
params, n_params)) < 0)
return res;
return 0;
}
static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
{
struct impl *impl = data;
pw_log_error("error id:%u seq:%d res:%d (%s): %s",
id, seq, res, spa_strerror(res), message);
if (id == PW_ID_CORE && res == -EPIPE)
pw_impl_module_schedule_destroy(impl->module);
}
static const struct pw_core_events core_events = {
PW_VERSION_CORE_EVENTS,
.error = core_error,
};
static void core_destroy(void *d)
{
struct impl *impl = d;
spa_hook_remove(&impl->core_listener);
impl->core = NULL;
pw_impl_module_schedule_destroy(impl->module);
}
static const struct pw_proxy_events core_proxy_events = {
.destroy = core_destroy,
};
static void impl_destroy(struct impl *impl)
{
if (impl->stream)
pw_stream_destroy(impl->stream);
if (impl->core && impl->do_disconnect)
pw_core_disconnect(impl->core);
pw_properties_free(impl->stream_props);
pw_properties_free(impl->props);
free(impl);
}
static void module_destroy(void *data)
{
struct impl *impl = data;
impl->unloading = true;
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,
};
static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
{
spa_audio_info_raw_init_dict_keys(info,
&SPA_DICT_ITEMS(
SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT),
SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)),
SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
&props->dict,
SPA_KEY_AUDIO_FORMAT,
SPA_KEY_AUDIO_RATE,
SPA_KEY_AUDIO_CHANNELS,
SPA_KEY_AUDIO_POSITION, NULL);
}
static int calc_frame_size(const struct spa_audio_info_raw *info)
{
int res = info->channels;
switch (info->format) {
case SPA_AUDIO_FORMAT_U8:
case SPA_AUDIO_FORMAT_S8:
case SPA_AUDIO_FORMAT_ALAW:
case SPA_AUDIO_FORMAT_ULAW:
return res;
case SPA_AUDIO_FORMAT_S16:
case SPA_AUDIO_FORMAT_S16_OE:
case SPA_AUDIO_FORMAT_U16:
return res * 2;
case SPA_AUDIO_FORMAT_S24:
case SPA_AUDIO_FORMAT_S24_OE:
case SPA_AUDIO_FORMAT_U24:
return res * 3;
case SPA_AUDIO_FORMAT_S24_32:
case SPA_AUDIO_FORMAT_S24_32_OE:
case SPA_AUDIO_FORMAT_S32:
case SPA_AUDIO_FORMAT_S32_OE:
case SPA_AUDIO_FORMAT_U32:
case SPA_AUDIO_FORMAT_U32_OE:
case SPA_AUDIO_FORMAT_F32:
case SPA_AUDIO_FORMAT_F32_OE:
return res * 4;
case SPA_AUDIO_FORMAT_F64:
case SPA_AUDIO_FORMAT_F64_OE:
return res * 8;
default:
return 0;
}
}
static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
{
const char *str;
if ((str = pw_properties_get(props, key)) != NULL) {
if (pw_properties_get(impl->stream_props, key) == NULL)
pw_properties_set(impl->stream_props, key, str);
}
}
SPA_EXPORT
int pipewire__module_init(struct pw_impl_module *module, const char *args)
{
struct pw_context *context = pw_impl_module_get_context(module);
uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
uint32_t pid = getpid();
struct pw_properties *props = NULL;
struct impl *impl;
const char *str;
int res;
PW_LOG_TOPIC_INIT(mod_topic);
impl = calloc(1, sizeof(struct impl));
if (impl == NULL)
return -errno;
pw_log_debug("module %p: new %s", impl, args);
if (args == NULL)
args = "";
props = pw_properties_new_string(args);
if (props == NULL) {
res = -errno;
pw_log_error( "can't create properties: %m");
goto error;
}
impl->props = props;
impl->stream_props = pw_properties_new(NULL, NULL);
if (impl->stream_props == NULL) {
res = -errno;
pw_log_error( "can't create properties: %m");
goto error;
}
impl->module = module;
impl->context = context;
if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source");
if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
pw_properties_setf(props, PW_KEY_NODE_NAME, "example-source-%u-%u", pid, id);
if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
pw_properties_set(props, PW_KEY_NODE_DESCRIPTION,
pw_properties_get(props, PW_KEY_NODE_NAME));
if ((str = pw_properties_get(props, "stream.props")) != NULL)
pw_properties_update_string(impl->stream_props, str, strlen(str));
copy_props(impl, props, PW_KEY_AUDIO_RATE);
copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
copy_props(impl, props, PW_KEY_NODE_NAME);
copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
copy_props(impl, props, PW_KEY_NODE_GROUP);
copy_props(impl, props, PW_KEY_NODE_LATENCY);
copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
copy_props(impl, props, PW_KEY_MEDIA_CLASS);
parse_audio_info(impl->stream_props, &impl->info);
impl->frame_size = calc_frame_size(&impl->info);
if (impl->frame_size == 0) {
res = -EINVAL;
pw_log_error( "can't parse audio format");
goto error;
}
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);
if ((res = create_stream(impl)) < 0)
goto error;
pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
return 0;
error:
impl_destroy(impl);
return res;
}