linux-pipewire: Add PipeWire audio captures

This commit is contained in:
Dimitris Papaioannou 2022-06-26 17:14:24 +03:00 committed by dimtpap
parent 600a564039
commit a6b3dc2106
No known key found for this signature in database
GPG key ID: EF8D77EBF9C1BD4F
7 changed files with 2480 additions and 0 deletions

View file

@ -31,6 +31,10 @@ target_sources(
formats.c
formats.h
linux-pipewire.c
pipewire-audio-capture-app.c
pipewire-audio-capture-device.c
pipewire-audio.c
pipewire-audio.h
pipewire.c
pipewire.h
portal.c

View file

@ -10,3 +10,12 @@ PipeWireSelectScreenCast="Open Selector"
Resolution="Resolution"
ShowCursor="Show Cursor"
VideoFormat="Video Format"
PipeWireAudioCaptureInput="Audio Input Capture (PipeWire)"
PipeWireAudioCaptureOutput="Audio Output Capture (PipeWire)"
PipeWireAudioCaptureApplication="Application Audio Capture (PipeWire)"
MatchPriority="Match Priority"
MatchBinaryFirst="Match by binary name, fallback to app name"
MatchAppNameFirst="Match by app name, fallback to binary name"
Device="Device"
Application="Application"
ExceptApp="Capture all apps except selected"

View file

@ -2,6 +2,7 @@
*
* Copyright 2021 columbarius <co1umbarius@protonmail.com>
* Copyright 2021 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
* Copyright 2022 Dimitris Papaioannou <dimtpap@protonmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -25,6 +26,7 @@
#include <pipewire/pipewire.h>
#include "screencast-portal.h"
#include "pipewire-audio.h"
#if PW_CHECK_VERSION(0, 3, 60)
#include "camera-portal.h"
@ -51,6 +53,9 @@ bool obs_module_load(void)
screencast_portal_load();
pipewire_audio_capture_load();
pipewire_audio_capture_app_load();
return true;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,525 @@
/* pipewire-audio-capture-device.c
*
* Copyright 2022-2024 Dimitris Papaioannou <dimtpap@protonmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "pipewire-audio.h"
#include <util/dstr.h>
/* Source for capturing device audio using PipeWire */
struct target_node {
const char *friendly_name;
const char *name;
uint32_t serial;
uint32_t id;
uint32_t channels;
struct spa_hook node_listener;
struct obs_pw_audio_capture_device *pwac;
};
enum capture_type {
INPUT,
OUTPUT,
};
struct obs_pw_audio_capture_device {
obs_source_t *source;
enum capture_type capture_type;
struct obs_pw_audio_instance pw;
struct {
struct obs_pw_audio_default_node_metadata metadata;
bool autoconnect;
uint32_t node_serial;
struct dstr name;
} default_info;
struct obs_pw_audio_proxy_list targets;
struct dstr target_name;
uint32_t connected_serial;
};
static void start_streaming(struct obs_pw_audio_capture_device *pwac,
struct target_node *node)
{
dstr_copy(&pwac->target_name, node->name);
if (pw_stream_get_state(pwac->pw.audio.stream, NULL) !=
PW_STREAM_STATE_UNCONNECTED) {
if (node->serial == pwac->connected_serial) {
/* Already connected to this node */
return;
}
pw_stream_disconnect(pwac->pw.audio.stream);
pwac->connected_serial = SPA_ID_INVALID;
}
if (!node->channels) {
return;
}
if (obs_pw_audio_stream_connect(&pwac->pw.audio, node->id, node->serial,
node->channels) == 0) {
pwac->connected_serial = node->serial;
blog(LOG_INFO, "[pipewire] %p streaming from %u",
pwac->pw.audio.stream, node->serial);
} else {
pwac->connected_serial = SPA_ID_INVALID;
blog(LOG_WARNING, "[pipewire] Error connecting stream %p",
pwac->pw.audio.stream);
}
pw_stream_set_active(pwac->pw.audio.stream,
obs_source_active(pwac->source));
}
struct target_node *get_node_by_name(struct obs_pw_audio_capture_device *pwac,
const char *name)
{
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->targets);
struct target_node *node;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {
if (strcmp(node->name, name) == 0) {
return node;
}
}
return NULL;
}
struct target_node *get_node_by_serial(struct obs_pw_audio_capture_device *pwac,
uint32_t serial)
{
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->targets);
struct target_node *node;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {
if (node->serial == serial) {
return node;
}
}
return NULL;
}
/* Target node */
static void on_node_info_cb(void *data, const struct pw_node_info *info)
{
if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) == 0 ||
!info->props || !info->props->n_items) {
return;
}
const char *prop_channels =
spa_dict_lookup(info->props, PW_KEY_AUDIO_CHANNELS);
if (!prop_channels) {
return;
}
uint32_t channels = strtoul(prop_channels, NULL, 10);
struct target_node *n = data;
if (n->channels == channels) {
return;
}
n->channels = channels;
struct obs_pw_audio_capture_device *pwac = n->pwac;
/** If this is the default device and the stream is not already connected to it
* or the stream is unconnected and this node has the desired target name */
if ((pwac->default_info.autoconnect &&
pwac->connected_serial != n->serial &&
!dstr_is_empty(&pwac->default_info.name) &&
dstr_cmp(&pwac->default_info.name, n->name) == 0) ||
(pw_stream_get_state(pwac->pw.audio.stream, NULL) ==
PW_STREAM_STATE_UNCONNECTED &&
!dstr_is_empty(&pwac->target_name) &&
dstr_cmp(&pwac->target_name, n->name) == 0)) {
start_streaming(pwac, n);
}
}
static const struct pw_node_events node_events = {
PW_VERSION_NODE_EVENTS,
.info = on_node_info_cb,
};
static void node_destroy_cb(void *data)
{
struct target_node *node = data;
struct obs_pw_audio_capture_device *pwac = node->pwac;
if (node->serial == pwac->connected_serial) {
if (pw_stream_get_state(pwac->pw.audio.stream, NULL) !=
PW_STREAM_STATE_UNCONNECTED) {
pw_stream_disconnect(pwac->pw.audio.stream);
}
pwac->connected_serial = SPA_ID_INVALID;
}
spa_hook_remove(&node->node_listener);
bfree((void *)node->friendly_name);
bfree((void *)node->name);
}
static void register_target_node(struct obs_pw_audio_capture_device *pwac,
const char *friendly_name, const char *name,
uint32_t object_serial, uint32_t global_id)
{
struct pw_proxy *node_proxy = pw_registry_bind(
pwac->pw.registry, global_id, PW_TYPE_INTERFACE_Node,
PW_VERSION_NODE, sizeof(struct target_node));
if (!node_proxy) {
return;
}
struct target_node *node = pw_proxy_get_user_data(node_proxy);
node->friendly_name = bstrdup(friendly_name);
node->name = bstrdup(name);
node->serial = object_serial;
node->channels = 0;
node->pwac = pwac;
obs_pw_audio_proxy_list_append(&pwac->targets, node_proxy);
spa_zero(node->node_listener);
pw_proxy_add_object_listener(node_proxy, &node->node_listener,
&node_events, node);
}
/* ------------------------------------------------- */
/* Default device metadata */
static void default_node_cb(void *data, const char *name)
{
struct obs_pw_audio_capture_device *pwac = data;
blog(LOG_DEBUG, "[pipewire] New default device %s", name);
dstr_copy(&pwac->default_info.name, name);
struct target_node *node = get_node_by_name(pwac, name);
if (node) {
pwac->default_info.node_serial = node->serial;
if (pwac->default_info.autoconnect) {
start_streaming(pwac, node);
}
} else {
blog(LOG_DEBUG,
"[pipewire] Failed to find node corresponding to default device");
}
}
/* ------------------------------------------------- */
/* Registry */
static void on_global_cb(void *data, uint32_t id, uint32_t permissions,
const char *type, uint32_t version,
const struct spa_dict *props)
{
UNUSED_PARAMETER(permissions);
UNUSED_PARAMETER(version);
struct obs_pw_audio_capture_device *pwac = data;
if (!props || !type) {
return;
}
if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
const char *node_name, *media_class;
if (!(node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME)) ||
!(media_class =
spa_dict_lookup(props, PW_KEY_MEDIA_CLASS))) {
return;
}
/* Target device */
if ((pwac->capture_type == INPUT &&
(strcmp(media_class, "Audio/Source") == 0 ||
strcmp(media_class, "Audio/Source/Virtual") == 0)) ||
(pwac->capture_type == OUTPUT &&
(strcmp(media_class, "Audio/Sink") == 0 ||
strcmp(media_class, "Audio/Duplex") == 0))) {
const char *prop_serial =
spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL);
if (!prop_serial) {
blog(LOG_WARNING,
"[pipewire] No object serial found on node %u",
id);
return;
}
uint32_t object_serial = strtoul(prop_serial, NULL, 10);
const char *node_friendly_name =
spa_dict_lookup(props, PW_KEY_NODE_NICK);
if (!node_friendly_name) {
node_friendly_name = spa_dict_lookup(
props, PW_KEY_NODE_DESCRIPTION);
if (!node_friendly_name) {
node_friendly_name = node_name;
}
}
register_target_node(pwac, node_friendly_name,
node_name, object_serial, id);
}
} else if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) {
const char *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME);
if (!name || strcmp(name, "default") != 0) {
return;
}
if (!obs_pw_audio_default_node_metadata_listen(
&pwac->default_info.metadata, &pwac->pw, id,
pwac->capture_type == OUTPUT, default_node_cb,
pwac)) {
blog(LOG_WARNING,
"[pipewire] Failed to get default metadata, cannot detect default audio devices");
}
}
}
static const struct pw_registry_events registry_events = {
PW_VERSION_REGISTRY_EVENTS,
.global = on_global_cb,
};
/* ------------------------------------------------- */
/* Source */
static void *pipewire_audio_capture_create(obs_data_t *settings,
obs_source_t *source,
enum capture_type capture_type)
{
struct obs_pw_audio_capture_device *pwac =
bzalloc(sizeof(struct obs_pw_audio_capture_device));
if (!obs_pw_audio_instance_init(&pwac->pw, &registry_events, pwac,
capture_type == OUTPUT, true, source)) {
obs_pw_audio_instance_destroy(&pwac->pw);
bfree(pwac);
return NULL;
}
pwac->source = source;
pwac->capture_type = capture_type;
pwac->default_info.node_serial = SPA_ID_INVALID;
pwac->connected_serial = SPA_ID_INVALID;
obs_pw_audio_proxy_list_init(&pwac->targets, NULL, node_destroy_cb);
if (obs_data_get_int(settings, "TargetSerial") != PW_ID_ANY) {
/** Reset id setting, PipeWire node ids may not persist between sessions.
* Connecting to saved target will happen based on the TargetName setting
* once target has connected */
obs_data_set_int(settings, "TargetSerial", 0);
} else {
pwac->default_info.autoconnect = true;
}
dstr_init_copy(&pwac->target_name,
obs_data_get_string(settings, "TargetName"));
obs_pw_audio_instance_sync(&pwac->pw);
pw_thread_loop_wait(pwac->pw.thread_loop);
pw_thread_loop_unlock(pwac->pw.thread_loop);
return pwac;
}
static void *pipewire_audio_capture_input_create(obs_data_t *settings,
obs_source_t *source)
{
return pipewire_audio_capture_create(settings, source, INPUT);
}
static void *pipewire_audio_capture_output_create(obs_data_t *settings,
obs_source_t *source)
{
return pipewire_audio_capture_create(settings, source, OUTPUT);
}
static void pipewire_audio_capture_defaults(obs_data_t *settings)
{
obs_data_set_default_int(settings, "TargetSerial", PW_ID_ANY);
}
static obs_properties_t *pipewire_audio_capture_properties(void *data)
{
struct obs_pw_audio_capture_device *pwac = data;
obs_properties_t *props = obs_properties_create();
obs_property_t *targets_list = obs_properties_add_list(
props, "TargetSerial", obs_module_text("Device"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
obs_property_list_add_int(targets_list, obs_module_text("Default"),
PW_ID_ANY);
if (!pwac->default_info.autoconnect) {
obs_data_t *settings = obs_source_get_settings(pwac->source);
/* Saved target serial may be different from connected because a previously connected
node may have been replaced by one with the same name */
obs_data_set_int(settings, "TargetSerial",
pwac->connected_serial);
obs_data_release(settings);
}
pw_thread_loop_lock(pwac->pw.thread_loop);
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->targets);
struct target_node *node;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {
obs_property_list_add_int(targets_list, node->friendly_name,
node->serial);
}
pw_thread_loop_unlock(pwac->pw.thread_loop);
return props;
}
static void pipewire_audio_capture_update(void *data, obs_data_t *settings)
{
struct obs_pw_audio_capture_device *pwac = data;
uint32_t new_node_serial = obs_data_get_int(settings, "TargetSerial");
pw_thread_loop_lock(pwac->pw.thread_loop);
if ((pwac->default_info.autoconnect = new_node_serial == PW_ID_ANY)) {
if (pwac->default_info.node_serial != SPA_ID_INVALID) {
start_streaming(
pwac,
get_node_by_serial(
pwac, pwac->default_info.node_serial));
}
} else {
struct target_node *new_node =
get_node_by_serial(pwac, new_node_serial);
if (new_node) {
start_streaming(pwac, new_node);
obs_data_set_string(settings, "TargetName",
pwac->target_name.array);
}
}
pw_thread_loop_unlock(pwac->pw.thread_loop);
}
static void pipewire_audio_capture_show(void *data)
{
struct obs_pw_audio_capture_device *pwac = data;
pw_thread_loop_lock(pwac->pw.thread_loop);
pw_stream_set_active(pwac->pw.audio.stream, true);
pw_thread_loop_unlock(pwac->pw.thread_loop);
}
static void pipewire_audio_capture_hide(void *data)
{
struct obs_pw_audio_capture_device *pwac = data;
pw_thread_loop_lock(pwac->pw.thread_loop);
pw_stream_set_active(pwac->pw.audio.stream, false);
pw_thread_loop_unlock(pwac->pw.thread_loop);
}
static void pipewire_audio_capture_destroy(void *data)
{
struct obs_pw_audio_capture_device *pwac = data;
pw_thread_loop_lock(pwac->pw.thread_loop);
obs_pw_audio_proxy_list_clear(&pwac->targets);
if (pwac->default_info.metadata.proxy) {
pw_proxy_destroy(pwac->default_info.metadata.proxy);
}
obs_pw_audio_instance_destroy(&pwac->pw);
dstr_free(&pwac->default_info.name);
dstr_free(&pwac->target_name);
bfree(pwac);
}
static const char *pipewire_audio_capture_input_name(void *data)
{
UNUSED_PARAMETER(data);
return obs_module_text("PipeWireAudioCaptureInput");
}
static const char *pipewire_audio_capture_output_name(void *data)
{
UNUSED_PARAMETER(data);
return obs_module_text("PipeWireAudioCaptureOutput");
}
void pipewire_audio_capture_load(void)
{
const struct obs_source_info pipewire_audio_capture_input = {
.id = "pipewire-audio-capture-input",
.type = OBS_SOURCE_TYPE_INPUT,
.output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE,
.get_name = pipewire_audio_capture_input_name,
.create = pipewire_audio_capture_input_create,
.get_defaults = pipewire_audio_capture_defaults,
.get_properties = pipewire_audio_capture_properties,
.update = pipewire_audio_capture_update,
.show = pipewire_audio_capture_show,
.hide = pipewire_audio_capture_hide,
.destroy = pipewire_audio_capture_destroy,
.icon_type = OBS_ICON_TYPE_AUDIO_INPUT,
};
const struct obs_source_info pipewire_audio_capture_output = {
.id = "pipewire-audio-capture-output",
.type = OBS_SOURCE_TYPE_INPUT,
.output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE |
OBS_SOURCE_DO_NOT_SELF_MONITOR,
.get_name = pipewire_audio_capture_output_name,
.create = pipewire_audio_capture_output_create,
.get_defaults = pipewire_audio_capture_defaults,
.get_properties = pipewire_audio_capture_properties,
.update = pipewire_audio_capture_update,
.show = pipewire_audio_capture_show,
.hide = pipewire_audio_capture_hide,
.destroy = pipewire_audio_capture_destroy,
.icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT,
};
obs_register_source(&pipewire_audio_capture_input);
obs_register_source(&pipewire_audio_capture_output);
}

View file

@ -0,0 +1,652 @@
/* pipewire-audio.c
*
* Copyright 2022-2024 Dimitris Papaioannou <dimtpap@protonmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "pipewire-audio.h"
#include <util/platform.h>
#include <spa/utils/json.h>
/* Utilities */
bool json_object_find(const char *obj, const char *key, char *value, size_t len)
{
/* From PipeWire's source */
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 false;
}
while (spa_json_get_string(&it[1], k, sizeof(k)) > 0) {
if (spa_streq(k, key)) {
if (spa_json_get_string(&it[1], value, len) > 0) {
return true;
}
} else if (spa_json_next(&it[1], &v) <= 0) {
break;
}
}
return false;
}
/* ------------------------------------------------- */
/* PipeWire stream wrapper */
void obs_channels_to_spa_audio_position(enum spa_audio_channel *position,
uint32_t channels)
{
switch (channels) {
case 1:
position[0] = SPA_AUDIO_CHANNEL_MONO;
break;
case 2:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
break;
case 3:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
position[2] = SPA_AUDIO_CHANNEL_LFE;
break;
case 4:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
position[2] = SPA_AUDIO_CHANNEL_FC;
position[3] = SPA_AUDIO_CHANNEL_RC;
break;
case 5:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
position[2] = SPA_AUDIO_CHANNEL_FC;
position[3] = SPA_AUDIO_CHANNEL_LFE;
position[4] = SPA_AUDIO_CHANNEL_RC;
break;
case 6:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
position[2] = SPA_AUDIO_CHANNEL_FC;
position[3] = SPA_AUDIO_CHANNEL_LFE;
position[4] = SPA_AUDIO_CHANNEL_RL;
position[5] = SPA_AUDIO_CHANNEL_RR;
break;
case 8:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
position[2] = SPA_AUDIO_CHANNEL_FC;
position[3] = SPA_AUDIO_CHANNEL_LFE;
position[4] = SPA_AUDIO_CHANNEL_RL;
position[5] = SPA_AUDIO_CHANNEL_RR;
position[6] = SPA_AUDIO_CHANNEL_SL;
position[7] = SPA_AUDIO_CHANNEL_SR;
break;
default:
for (size_t i = 0; i < channels; i++) {
position[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
}
break;
}
}
enum audio_format spa_to_obs_audio_format(enum spa_audio_format format)
{
switch (format) {
case SPA_AUDIO_FORMAT_U8:
return AUDIO_FORMAT_U8BIT;
case SPA_AUDIO_FORMAT_S16_LE:
return AUDIO_FORMAT_16BIT;
case SPA_AUDIO_FORMAT_S32_LE:
return AUDIO_FORMAT_32BIT;
case SPA_AUDIO_FORMAT_F32_LE:
return AUDIO_FORMAT_FLOAT;
case SPA_AUDIO_FORMAT_U8P:
return AUDIO_FORMAT_U8BIT_PLANAR;
case SPA_AUDIO_FORMAT_S16P:
return AUDIO_FORMAT_16BIT_PLANAR;
case SPA_AUDIO_FORMAT_S32P:
return AUDIO_FORMAT_32BIT_PLANAR;
case SPA_AUDIO_FORMAT_F32P:
return AUDIO_FORMAT_FLOAT_PLANAR;
default:
return AUDIO_FORMAT_UNKNOWN;
}
}
enum speaker_layout spa_to_obs_speakers(uint32_t channels)
{
switch (channels) {
case 1:
return SPEAKERS_MONO;
case 2:
return SPEAKERS_STEREO;
case 3:
return SPEAKERS_2POINT1;
case 4:
return SPEAKERS_4POINT0;
case 5:
return SPEAKERS_4POINT1;
case 6:
return SPEAKERS_5POINT1;
case 8:
return SPEAKERS_7POINT1;
default:
return SPEAKERS_UNKNOWN;
}
}
bool spa_to_obs_pw_audio_info(struct obs_pw_audio_info *info,
const struct spa_pod *param)
{
struct spa_audio_info_raw audio_info;
if (spa_format_audio_raw_parse(param, &audio_info) < 0) {
info->sample_rate = 0;
info->format = AUDIO_FORMAT_UNKNOWN;
info->speakers = SPEAKERS_UNKNOWN;
return false;
}
info->sample_rate = audio_info.rate;
info->speakers = spa_to_obs_speakers(audio_info.channels);
info->format = spa_to_obs_audio_format(audio_info.format);
return true;
}
static void on_process_cb(void *data)
{
uint64_t now = os_gettime_ns();
struct obs_pw_audio_stream *s = data;
struct pw_buffer *b = pw_stream_dequeue_buffer(s->stream);
if (!b) {
return;
}
struct spa_buffer *buf = b->buffer;
if (!s->info.sample_rate || buf->n_datas == 0 ||
buf->datas[0].chunk->stride == 0 ||
buf->datas[0].type != SPA_DATA_MemPtr) {
goto queue;
}
struct obs_source_audio out = {
.frames =
buf->datas[0].chunk->size / buf->datas[0].chunk->stride,
.speakers = s->info.speakers,
.format = s->info.format,
.samples_per_sec = s->info.sample_rate,
};
for (size_t i = 0; i < buf->n_datas && i < MAX_AV_PLANES; i++) {
out.data[i] = buf->datas[i].data;
}
if (s->info.sample_rate * s->pos->clock.rate_diff) {
/** Taken from PipeWire's implementation of JACK's jack_get_cycle_times
* (https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/0.3.52/pipewire-jack/src/pipewire-jack.c#L5639)
* which is used in the linux-jack plugin to correctly set the timestamp
* (https://github.com/obsproject/obs-studio/blob/27.2.4/plugins/linux-jack/jack-wrapper.c#L87) */
double period_nsecs =
s->pos->clock.duration * (double)SPA_NSEC_PER_SEC /
(s->info.sample_rate * s->pos->clock.rate_diff);
out.timestamp = now - (uint64_t)period_nsecs;
} else {
out.timestamp = now - audio_frames_to_ns(s->info.sample_rate,
out.frames);
}
obs_source_output_audio(s->output, &out);
queue:
pw_stream_queue_buffer(s->stream, b);
}
static void on_state_changed_cb(void *data, enum pw_stream_state old,
enum pw_stream_state state, const char *error)
{
UNUSED_PARAMETER(old);
struct obs_pw_audio_stream *s = data;
blog(LOG_DEBUG, "[pipewire] Stream %p state: \"%s\" (error: %s)",
s->stream, pw_stream_state_as_string(state),
error ? error : "none");
}
static void on_param_changed_cb(void *data, uint32_t id,
const struct spa_pod *param)
{
if (!param || id != SPA_PARAM_Format) {
return;
}
struct obs_pw_audio_stream *s = data;
if (!spa_to_obs_pw_audio_info(&s->info, param)) {
blog(LOG_WARNING,
"[pipewire] Stream %p failed to parse audio format info",
s->stream);
} else {
blog(LOG_INFO,
"[pipewire] %p Got format: rate %u - channels %u - format %u",
s->stream, s->info.sample_rate, s->info.speakers,
s->info.format);
}
}
static void on_io_changed_cb(void *data, uint32_t id, void *area, uint32_t size)
{
UNUSED_PARAMETER(size);
struct obs_pw_audio_stream *s = data;
if (id == SPA_IO_Position) {
s->pos = area;
}
}
static const struct pw_stream_events stream_events = {
PW_VERSION_STREAM_EVENTS,
.process = on_process_cb,
.state_changed = on_state_changed_cb,
.param_changed = on_param_changed_cb,
.io_changed = on_io_changed_cb,
};
int obs_pw_audio_stream_connect(struct obs_pw_audio_stream *s,
uint32_t target_id, uint32_t target_serial,
uint32_t audio_channels)
{
enum spa_audio_channel pos[8];
obs_channels_to_spa_audio_position(pos, audio_channels);
uint8_t buffer[2048];
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const struct spa_pod *params[1];
params[0] = spa_pod_builder_add_object(
&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
SPA_FORMAT_AUDIO_channels, SPA_POD_Int(audio_channels),
SPA_FORMAT_AUDIO_position,
SPA_POD_Array(sizeof(enum spa_audio_channel), SPA_TYPE_Id,
audio_channels, pos),
SPA_FORMAT_AUDIO_format,
SPA_POD_CHOICE_ENUM_Id(
8, SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_S16_LE,
SPA_AUDIO_FORMAT_S32_LE, SPA_AUDIO_FORMAT_F32_LE,
SPA_AUDIO_FORMAT_U8P, SPA_AUDIO_FORMAT_S16P,
SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_F32P));
struct pw_properties *stream_props = pw_properties_new(NULL, NULL);
pw_properties_setf(stream_props, PW_KEY_TARGET_OBJECT, "%u",
target_serial);
pw_stream_update_properties(s->stream, &stream_props->dict);
pw_properties_free(stream_props);
return pw_stream_connect(s->stream, PW_DIRECTION_INPUT, target_id,
PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS |
PW_STREAM_FLAG_DONT_RECONNECT,
params, 1);
}
/* ------------------------------------------------- */
/* Common PipeWire components */
static void on_core_done_cb(void *data, uint32_t id, int seq)
{
struct obs_pw_audio_instance *pw = data;
if (id == PW_ID_CORE && pw->seq == seq) {
pw_thread_loop_signal(pw->thread_loop, false);
}
}
static void on_core_error_cb(void *data, uint32_t id, int seq, int res,
const char *message)
{
struct obs_pw_audio_instance *pw = data;
blog(LOG_ERROR, "[pipewire] Error id:%u seq:%d res:%d :%s", id, seq,
res, message);
pw_thread_loop_signal(pw->thread_loop, false);
}
static const struct pw_core_events core_events = {
PW_VERSION_CORE_EVENTS,
.done = on_core_done_cb,
.error = on_core_error_cb,
};
bool obs_pw_audio_instance_init(
struct obs_pw_audio_instance *pw,
const struct pw_registry_events *registry_events,
void *registry_cb_data, bool stream_capture_sink,
bool stream_want_driver, obs_source_t *stream_output)
{
pw->thread_loop = pw_thread_loop_new("PipeWire thread loop", NULL);
pw->context = pw_context_new(pw_thread_loop_get_loop(pw->thread_loop),
NULL, 0);
pw_thread_loop_lock(pw->thread_loop);
if (pw_thread_loop_start(pw->thread_loop) < 0) {
blog(LOG_WARNING,
"[pipewire] Error starting threaded mainloop");
return false;
}
pw->core = pw_context_connect(pw->context, NULL, 0);
if (!pw->core) {
blog(LOG_WARNING, "[pipewire] Error creating PipeWire core");
return false;
}
pw_core_add_listener(pw->core, &pw->core_listener, &core_events, pw);
pw->registry = pw_core_get_registry(pw->core, PW_VERSION_REGISTRY, 0);
if (!pw->registry) {
return false;
}
pw_registry_add_listener(pw->registry, &pw->registry_listener,
registry_events, registry_cb_data);
pw->audio.output = stream_output;
pw->audio.stream = pw_stream_new(
pw->core, "OBS",
pw_properties_new(
PW_KEY_NODE_NAME, "OBS", PW_KEY_NODE_DESCRIPTION,
"OBS Audio Capture", PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE,
"Production", PW_KEY_NODE_WANT_DRIVER,
stream_want_driver ? "true" : "false",
PW_KEY_STREAM_CAPTURE_SINK,
stream_capture_sink ? "true" : "false", NULL));
if (!pw->audio.stream) {
blog(LOG_WARNING, "[pipewire] Failed to create stream");
return false;
}
blog(LOG_INFO, "[pipewire] Created stream %p", pw->audio.stream);
pw_stream_add_listener(pw->audio.stream, &pw->audio.stream_listener,
&stream_events, &pw->audio);
return true;
}
void obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw)
{
if (pw->audio.stream) {
spa_hook_remove(&pw->audio.stream_listener);
if (pw_stream_get_state(pw->audio.stream, NULL) !=
PW_STREAM_STATE_UNCONNECTED) {
pw_stream_disconnect(pw->audio.stream);
}
pw_stream_destroy(pw->audio.stream);
}
if (pw->registry) {
spa_hook_remove(&pw->registry_listener);
spa_zero(pw->registry_listener);
pw_proxy_destroy((struct pw_proxy *)pw->registry);
}
pw_thread_loop_unlock(pw->thread_loop);
pw_thread_loop_stop(pw->thread_loop);
if (pw->core) {
spa_hook_remove(&pw->core_listener);
spa_zero(pw->core_listener);
pw_core_disconnect(pw->core);
}
if (pw->context) {
pw_context_destroy(pw->context);
}
pw_thread_loop_destroy(pw->thread_loop);
}
void obs_pw_audio_instance_sync(struct obs_pw_audio_instance *pw)
{
pw->seq = pw_core_sync(pw->core, PW_ID_CORE, pw->seq);
}
/* ------------------------------------------------- */
/* PipeWire metadata */
static int on_metadata_property_cb(void *data, uint32_t id, const char *key,
const char *type, const char *value)
{
UNUSED_PARAMETER(type);
struct obs_pw_audio_default_node_metadata *metadata = data;
if (id == PW_ID_CORE && key && value &&
strcmp(key, metadata->wants_sink ? "default.audio.sink"
: "default.audio.source") == 0) {
char val[128];
if (json_object_find(value, "name", val, sizeof(val)) && *val) {
metadata->default_node_callback(metadata->data, val);
}
}
return 0;
}
static const struct pw_metadata_events metadata_events = {
PW_VERSION_METADATA_EVENTS,
.property = on_metadata_property_cb,
};
static void on_metadata_proxy_removed_cb(void *data)
{
struct obs_pw_audio_default_node_metadata *metadata = data;
pw_proxy_destroy(metadata->proxy);
}
static void on_metadata_proxy_destroy_cb(void *data)
{
struct obs_pw_audio_default_node_metadata *metadata = data;
spa_hook_remove(&metadata->metadata_listener);
spa_hook_remove(&metadata->proxy_listener);
spa_zero(metadata->metadata_listener);
spa_zero(metadata->proxy_listener);
metadata->proxy = NULL;
}
static const struct pw_proxy_events metadata_proxy_events = {
PW_VERSION_PROXY_EVENTS,
.removed = on_metadata_proxy_removed_cb,
.destroy = on_metadata_proxy_destroy_cb,
};
bool obs_pw_audio_default_node_metadata_listen(
struct obs_pw_audio_default_node_metadata *metadata,
struct obs_pw_audio_instance *pw, uint32_t global_id, bool wants_sink,
void (*default_node_callback)(void *data, const char *name), void *data)
{
if (metadata->proxy) {
pw_proxy_destroy(metadata->proxy);
}
struct pw_proxy *metadata_proxy = pw_registry_bind(
pw->registry, global_id, PW_TYPE_INTERFACE_Metadata,
PW_VERSION_METADATA, 0);
if (!metadata_proxy) {
return false;
}
metadata->proxy = metadata_proxy;
metadata->wants_sink = wants_sink;
metadata->default_node_callback = default_node_callback;
metadata->data = data;
pw_proxy_add_object_listener(metadata->proxy,
&metadata->metadata_listener,
&metadata_events, metadata);
pw_proxy_add_listener(metadata->proxy, &metadata->proxy_listener,
&metadata_proxy_events, metadata);
return true;
}
/* ------------------------------------------------- */
/* Proxied objects */
struct obs_pw_audio_proxied_object {
void (*bound_callback)(void *data, uint32_t global_id);
void (*destroy_callback)(void *data);
struct pw_proxy *proxy;
struct spa_hook proxy_listener;
struct spa_list link;
};
static void on_proxy_bound_cb(void *data, uint32_t global_id)
{
struct obs_pw_audio_proxied_object *obj = data;
if (obj->bound_callback) {
obj->bound_callback(pw_proxy_get_user_data(obj->proxy),
global_id);
}
}
static void on_proxy_removed_cb(void *data)
{
struct obs_pw_audio_proxied_object *obj = data;
pw_proxy_destroy(obj->proxy);
}
static void on_proxy_destroy_cb(void *data)
{
struct obs_pw_audio_proxied_object *obj = data;
spa_hook_remove(&obj->proxy_listener);
spa_list_remove(&obj->link);
if (obj->destroy_callback) {
obj->destroy_callback(pw_proxy_get_user_data(obj->proxy));
}
bfree(data);
}
static const struct pw_proxy_events proxy_events = {
PW_VERSION_PROXY_EVENTS,
.bound = on_proxy_bound_cb,
.removed = on_proxy_removed_cb,
.destroy = on_proxy_destroy_cb,
};
void obs_pw_audio_proxied_object_new(struct pw_proxy *proxy,
struct spa_list *list,
void (*bound_callback)(void *data,
uint32_t global_id),
void (*destroy_callback)(void *data))
{
struct obs_pw_audio_proxied_object *obj =
bmalloc(sizeof(struct obs_pw_audio_proxied_object));
obj->proxy = proxy;
obj->bound_callback = bound_callback;
obj->destroy_callback = destroy_callback;
spa_list_append(list, &obj->link);
spa_zero(obj->proxy_listener);
pw_proxy_add_listener(obj->proxy, &obj->proxy_listener, &proxy_events,
obj);
}
void *obs_pw_audio_proxied_object_get_user_data(
struct obs_pw_audio_proxied_object *obj)
{
return pw_proxy_get_user_data(obj->proxy);
}
void obs_pw_audio_proxy_list_init(struct obs_pw_audio_proxy_list *list,
void (*bound_callback)(void *data,
uint32_t global_id),
void (*destroy_callback)(void *data))
{
spa_list_init(&list->list);
list->bound_callback = bound_callback;
list->destroy_callback = destroy_callback;
}
void obs_pw_audio_proxy_list_append(struct obs_pw_audio_proxy_list *list,
struct pw_proxy *proxy)
{
obs_pw_audio_proxied_object_new(proxy, &list->list,
list->bound_callback,
list->destroy_callback);
}
void obs_pw_audio_proxy_list_clear(struct obs_pw_audio_proxy_list *list)
{
struct obs_pw_audio_proxied_object *obj, *temp;
spa_list_for_each_safe(obj, temp, &list->list, link)
{
pw_proxy_destroy(obj->proxy);
}
}
void obs_pw_audio_proxy_list_iter_init(struct obs_pw_audio_proxy_list_iter *iter,
struct obs_pw_audio_proxy_list *list)
{
iter->proxy_list = list;
iter->current = spa_list_first(
&list->list, struct obs_pw_audio_proxied_object, link);
}
bool obs_pw_audio_proxy_list_iter_next(
struct obs_pw_audio_proxy_list_iter *iter, void **proxy_user_data)
{
if (spa_list_is_empty(&iter->proxy_list->list)) {
return false;
}
if (spa_list_is_end(iter->current, &iter->proxy_list->list, link)) {
return false;
}
*proxy_user_data =
obs_pw_audio_proxied_object_get_user_data(iter->current);
iter->current = spa_list_next(iter->current, link);
return true;
}
/* ------------------------------------------------- */

View file

@ -0,0 +1,185 @@
/* pipewire-audio.h
*
* Copyright 2022-2024 Dimitris Papaioannou <dimtpap@protonmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
/* Stuff used by the PipeWire audio capture sources */
#pragma once
#include <obs-module.h>
#include <pipewire/pipewire.h>
#include <pipewire/extensions/metadata.h>
#include <spa/param/audio/format-utils.h>
/* PipeWire Stream wrapper */
/**
* Audio metadata
*/
struct obs_pw_audio_info {
uint32_t sample_rate;
enum audio_format format;
enum speaker_layout speakers;
};
/**
* PipeWire stream wrapper that outputs to an OBS source
*/
struct obs_pw_audio_stream {
struct pw_stream *stream;
struct spa_hook stream_listener;
struct obs_pw_audio_info info;
struct spa_io_position *pos;
obs_source_t *output;
};
/**
* Connect a stream with the default params
* @return 0 on success, < 0 on error
*/
int obs_pw_audio_stream_connect(struct obs_pw_audio_stream *s,
uint32_t target_id, uint32_t target_serial,
uint32_t channels);
/* ------------------------------------------------- */
/**
* Common PipeWire components
*/
struct obs_pw_audio_instance {
struct pw_thread_loop *thread_loop;
struct pw_context *context;
struct pw_core *core;
struct spa_hook core_listener;
int seq;
struct pw_registry *registry;
struct spa_hook registry_listener;
struct obs_pw_audio_stream audio;
};
/**
* Initialize a PipeWire instance
* @warning The thread loop is left locked
* @return true on success, false on error
*/
bool obs_pw_audio_instance_init(
struct obs_pw_audio_instance *pw,
const struct pw_registry_events *registry_events,
void *registry_cb_data, bool stream_capture_sink,
bool stream_want_driver, obs_source_t *stream_output);
/**
* Destroy a PipeWire instance
* @warning Call with the thread loop locked
*/
void obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw);
/**
* Trigger a PipeWire core sync
*/
void obs_pw_audio_instance_sync(struct obs_pw_audio_instance *pw);
/* ------------------------------------------------- */
/**
* PipeWire metadata
*/
struct obs_pw_audio_default_node_metadata {
struct pw_proxy *proxy;
struct spa_hook proxy_listener;
struct spa_hook metadata_listener;
bool wants_sink;
void (*default_node_callback)(void *data, const char *name);
void *data;
};
/**
* Add listeners to the metadata
* @return true on success, false on error
*/
bool obs_pw_audio_default_node_metadata_listen(
struct obs_pw_audio_default_node_metadata *metadata,
struct obs_pw_audio_instance *pw, uint32_t global_id, bool wants_sink,
void (*default_node_callback)(void *data, const char *name),
void *data);
/* ------------------------------------------------- */
/* Helpers for storing remote PipeWire objects */
/**
* Wrapper over a PipeWire proxy that's a member of a spa_list.
* Automatically handles adding and removing itself from the list.
*/
struct obs_pw_audio_proxied_object;
/**
* Get the user data of a proxied object
*/
void *obs_pw_audio_proxied_object_get_user_data(
struct obs_pw_audio_proxied_object *obj);
/**
* Convenience wrapper over spa_lists that holds proxied objects
*/
struct obs_pw_audio_proxy_list {
struct spa_list list;
void (*bound_callback)(void *data, uint32_t global_id);
void (*destroy_callback)(void *data);
};
void obs_pw_audio_proxy_list_init(struct obs_pw_audio_proxy_list *list,
void (*bound_callback)(void *data,
uint32_t global_id),
void (*destroy_callback)(void *data));
void obs_pw_audio_proxy_list_append(struct obs_pw_audio_proxy_list *list,
struct pw_proxy *proxy);
/**
* Destroy all stored proxies.
*/
void obs_pw_audio_proxy_list_clear(struct obs_pw_audio_proxy_list *list);
/**
* Iterator over all user data of the proxies in the list
*/
struct obs_pw_audio_proxy_list_iter {
struct obs_pw_audio_proxy_list *proxy_list;
struct obs_pw_audio_proxied_object *current;
};
void obs_pw_audio_proxy_list_iter_init(struct obs_pw_audio_proxy_list_iter *iter,
struct obs_pw_audio_proxy_list *list);
/**
* @return true when there are more items to process, false otherwise
*/
bool obs_pw_audio_proxy_list_iter_next(
struct obs_pw_audio_proxy_list_iter *iter, void **proxy_user_data);
/* ------------------------------------------------- */
/* Sources */
void pipewire_audio_capture_load(void);
void pipewire_audio_capture_app_load(void);
/* ------------------------------------------------- */