alsa: Support enumerating virtual PCM sinks

Add support to the ALSA device provider to enumerate PCM outputs that do
not correspond to a physical sound device i.e. they are "virtual" sinks,
like the plug, dmix, or softvol PCM outputs that can be setup in the ALSA
configuration files.

The main use-case for this is allowing usage of GstDeviceMonitor in setups
where there is no audio server and have custom ALSA audio configurations.
As those are likely to be uncommon, the feature is opt-in: a list of device
names and wildcard patterns separated by semicolons must be assigned to the
GST_ALSA_PCM_ALLOW environment variable before such PCM outputs will be
enumerated by the ALSA device provider. This allows either scanning all
PCM outputs, listing individual outputs, providing simple patterns with
'*' wildcards (which match only at the start or end of the name), or
a combination of them:

  GST_ALSA_PCM_ALLOW=1                         # Enable listing PCM outputs.
  GST_ALSA_PCM_ALLOW='*'                       # Same, using a wildcard.
  GST_ALSA_PCM_ALLOW='out_1;out_1'             # Exact listing.
  GST_ALSA_PCM_ALLOW='out_*'                   # Using a wildcard.
  GST_ALSA_PCM_ALLOW='out_*;other_*;line_out'  # Multiple items.

The main motivation for this patch is supporting enumeration of PCM outputs
in the WebKit GTK and WPE ports, which use GstDeviceMonitor to determine
which devices may be chosen for sound output. While on desktops typically
PulseAudio or PipeWire are used nowadays, on embedded devices it is often
desirable to avoid them and use custom configurations that perform audio
routing and processing using only ALSA.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/8831>
This commit is contained in:
Adrian Perez de Castro 2025-04-13 19:53:03 +02:00
parent d33107226c
commit 3968dd92a5

View File

@ -24,10 +24,8 @@
#endif
#include "gstalsadeviceprovider.h"
#include <string.h>
#include <gst/gst.h>
static GstDevice *gst_alsa_device_new (const gchar * device_name,
GstCaps * caps, const gchar * internal_name, snd_pcm_stream_t stream,
GstStructure * properties);
@ -96,6 +94,171 @@ add_device (GstDeviceProvider * provider, snd_ctl_t * info,
return device;
}
static GList *
gst_alsa_parse_pcm_allow_patterns (const gchar * patterns)
{
if (!patterns)
return NULL;
GList *pattern_list = NULL;
gchar *patterns_trimmed = g_strchomp (g_strdup (patterns));
if (g_strcmp0 (patterns_trimmed, "1") == 0) {
pattern_list = g_list_append (pattern_list, g_strdup ("*"));
} else {
gchar **pattern_strings = g_strsplit (patterns_trimmed, ";", -1);
for (size_t i = 0; pattern_strings[i]; i++) {
gchar *pattern = g_strchomp (pattern_strings[i]);
if (*pattern)
pattern_list = g_list_append (pattern_list, g_strdup (pattern));
}
g_strfreev (pattern_strings);
}
g_free (patterns_trimmed);
return pattern_list;
}
static gboolean
gst_alsa_pcm_name_matches_pattern (const gchar * name, const gchar * pattern)
{
g_assert (name);
g_assert (pattern);
size_t pattern_len = strlen (pattern);
g_assert (pattern_len > 0);
if (pattern[0] == '*') {
if (pattern_len == 1) /* pattern == "*", matches any input */
return TRUE;
if (pattern[pattern_len - 1] == '*') {
if (pattern_len == 2) /* pattern == "**", matches any input */
return TRUE;
/* pattern == "*<text>*", matches if <text> is contained in the name */
g_autofree gchar *needle = g_strndup (pattern + 1, pattern_len - 2);
return strstr (name, needle) != NULL;
}
/* pattern == "*<text>", matches if <text> is the name suffix */
return g_str_has_suffix (name, pattern + 1);
}
/* pattern == "<text>*", matches if <text> is the name prefix */
if (pattern[pattern_len - 1] == '*') {
g_autofree gchar *prefix = g_strndup (pattern, pattern_len - 1);
return g_str_has_prefix (name, prefix);
}
/* pattern == "<text>", matches if <text> is the same as the name */
return strcmp (name, pattern) == 0;
}
static gboolean
gst_alsa_pcm_name_matches_any_pattern (const gchar * name, GList * patterns)
{
g_assert (name);
g_assert (patterns);
for (GList * item = g_list_first (patterns); item; item = g_list_next (item))
if (gst_alsa_pcm_name_matches_pattern (name, item->data))
return TRUE;
return FALSE;
}
static GList *
gst_alsa_device_provider_probe_pcm_sinks (GstDeviceProvider * provider,
GList * list)
{
GList *allow_patterns =
gst_alsa_parse_pcm_allow_patterns (g_getenv ("GST_ALSA_PCM_ALLOW"));
if (!allow_patterns)
return list;
void **hints;
if (snd_device_name_hint (-1, "pcm", &hints) < 0)
goto beach;
snd_pcm_info_t *pcm_info;
snd_pcm_info_malloc (&pcm_info);
for (void **n = hints; *n; n++) {
char *name = snd_device_name_get_hint (*n, "NAME");
char *desc = snd_device_name_get_hint (*n, "DESC");
char *io = snd_device_name_get_hint (*n, "IOID");
if (!gst_alsa_pcm_name_matches_any_pattern (name, allow_patterns))
goto next_hint;
/*
* Skip devices without description or that have a valid IOID hint.
* The latter seems to be always NULL for "virtual" PCM sinks.
*/
if (!desc || io) {
GST_DEBUG_OBJECT (provider, "No io or desc hint for %s", name);
goto next_hint;
}
snd_pcm_t *pcm_handle = NULL;
int err = snd_pcm_open (&pcm_handle, name, SND_PCM_STREAM_PLAYBACK,
SND_PCM_NONBLOCK);
if (err < 0) {
GST_ERROR_OBJECT (provider,
"Could not open PCM device '%s' for inspection! (%s)", name,
snd_strerror (err));
goto next_hint;
}
if ((err = snd_pcm_info (pcm_handle, pcm_info)) < 0) {
GST_WARNING_OBJECT (provider,
"Cannot obtain PCM info for device '%s' (%s)", name,
snd_strerror (err));
snd_pcm_close (pcm_handle);
goto next_hint;
}
GstCaps *template = gst_static_caps_get (&alsa_caps);
GstCaps *caps = gst_alsa_probe_supported_formats (GST_OBJECT (provider),
name, pcm_handle, template);
g_clear_pointer (&template, gst_caps_unref);
GstStructure *props = gst_structure_new ("alsa-proplist",
"device.api", G_TYPE_STRING, "alsa",
"device.class", G_TYPE_STRING, "sound",
NULL);
GstAlsaDevice *gstdev = g_object_new (GST_TYPE_ALSA_DEVICE,
"display-name", desc,
"caps", caps,
"device-class", "Audio/Sink",
"internal-name", name,
"properties", props,
NULL);
gstdev->stream = SND_PCM_STREAM_PLAYBACK;
gstdev->element = "alsasink";
gst_clear_structure (&props);
gst_clear_caps (&caps);
snd_pcm_close (pcm_handle);
list = g_list_prepend (list, gstdev);
next_hint:
free (name);
free (desc);
free (io);
}
snd_pcm_info_free (pcm_info);
snd_device_name_free_hint (hints);
g_list_free_full (allow_patterns, g_free);
beach:
return list;
}
static GList *
gst_alsa_device_provider_probe (GstDeviceProvider * provider)
{
@ -164,7 +327,7 @@ beach:
snd_ctl_card_info_free (info);
snd_pcm_info_free (pcminfo);
return list;
return gst_alsa_device_provider_probe_pcm_sinks (provider, list);
}