mirror of
https://github.com/obsproject/obs-studio.git
synced 2024-07-02 09:33:33 +00:00
linux-pipewire: Add PipeWire audio captures
This commit is contained in:
parent
600a564039
commit
a6b3dc2106
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
1100
plugins/linux-pipewire/pipewire-audio-capture-app.c
Normal file
1100
plugins/linux-pipewire/pipewire-audio-capture-app.c
Normal file
File diff suppressed because it is too large
Load diff
525
plugins/linux-pipewire/pipewire-audio-capture-device.c
Normal file
525
plugins/linux-pipewire/pipewire-audio-capture-device.c
Normal 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, ®istry_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);
|
||||
}
|
652
plugins/linux-pipewire/pipewire-audio.c
Normal file
652
plugins/linux-pipewire/pipewire-audio.c
Normal 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;
|
||||
}
|
||||
/* ------------------------------------------------- */
|
185
plugins/linux-pipewire/pipewire-audio.h
Normal file
185
plugins/linux-pipewire/pipewire-audio.h
Normal 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);
|
||||
/* ------------------------------------------------- */
|
Loading…
Reference in a new issue