/*
* Copyright (c) 2011- Osmo Antero.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 3 of the License (GPL3), or any later version.
*
* This library 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 Library General Public License 3 for more details.
*
* You should have received a copy of the GNU Library General Public
* License 3 along with this program; if not, see /usr/share/common-licenses/GPL file
* or .
*/
#include "gst-devices.h"
#include "support.h" // _(x)
#include "log.h"
#include "utility.h"
#include "rec-manager-struct.h"
#include
#include
#include
// Collect audio input devices and audio output devices w/ connected loudspeakers.
// This module is using gst_device_monitor_* functions that were introduced in GStreamer 1.4.
// You may also check the output of these pa (pulseaudio) commands.
// Get list of input devices:
// $ pactl list short sources | cut -f2
// $ pactl list | grep -A6 'Source #' | egrep "Name: |Description: "
//
// Get list of sink (output) devices:
// $ pactl list short sinks | cut -f2
// $ pactl list | grep -A6 'Sink #' | egrep "Name: |Description: "
// Audio source devices
G_LOCK_DEFINE_STATIC(g_source_list);
static GList *g_source_list = NULL;
static GstDeviceMonitor *g_dev_monitor = NULL;
static void gstdev_get_devices();
static void gstdev_read_fields(GstDevice *dev, gchar **dev_id, gchar **id_attrib, gchar **dev_descr, gchar **media_role, gchar **dev_class, gchar **icon_name);
static void gstdev_add_to_list(GstDevice *dev);
static void gstdev_remove_from_list(GstDevice *dev);
static void gstdev_clear_lists();
void gstdev_module_init() {
LOG_DEBUG("Init gst-devices.c.\n");
g_source_list = NULL;
g_dev_monitor = NULL;
}
void gstdev_module_exit() {
LOG_DEBUG("Clean up gst-devices.c.\n");
if (GST_IS_DEVICE_MONITOR(g_dev_monitor)) {
gst_device_monitor_stop(g_dev_monitor);
gst_object_unref(g_dev_monitor);
}
g_dev_monitor = NULL;
// Clear lists
gstdev_clear_lists();
}
GList *gstdev_get_source_list() {
// Return g_source_list to the caller
gstdev_get_devices();
return g_source_list;
}
static void gstdev_clear_lists() {
LOG_DEBUG("gstdev_clear_lists(). Clear g_source_list.\n");
G_LOCK(g_source_list);
// Free g_source_list
audio_sources_free_list(g_source_list);
g_source_list = NULL;
G_UNLOCK(g_source_list);
}
void gstdev_update_GUI() {
// Device list changed. Update GUI.
RecorderCommand *cmd = g_malloc0(sizeof(RecorderCommand));
cmd->type = RECORDING_DEVICE_CHANGED;
// Send command to rec-manager.c and GUI.
// It will free the cmd structure after processing.
rec_manager_send_command(cmd);
}
static gboolean message_func(GstBus *bus, GstMessage *message, gpointer user_data) {
GstDevice *device = NULL;
gchar *name = NULL;
LOG_DEBUG("message_func(): function to add or remove device called.\n");
switch (GST_MESSAGE_TYPE (message)) {
case GST_MESSAGE_DEVICE_ADDED:
gst_message_parse_device_added(message, &device);
name = gst_device_get_display_name(device);
LOG_DEBUG("Audio device added: %s\n", name);
g_free (name);
gstdev_add_to_list(device);
gstdev_update_GUI();
break;
case GST_MESSAGE_DEVICE_REMOVED:
gst_message_parse_device_removed(message, &device);
name = gst_device_get_display_name(device);
LOG_DEBUG("Audio device removed: %s\n", name);
g_free (name);
gstdev_remove_from_list(device);
gstdev_update_GUI();
break;
default:
break;
}
return G_SOURCE_CONTINUE;
}
GstDeviceMonitor *setup_raw_audio_source_device_monitor() {
LOG_DEBUG("Setup monitor to detect new and unplugged devices.\n");
GstDeviceMonitor *monitor = gst_device_monitor_new ();
GstBus *bus = gst_device_monitor_get_bus(monitor);
gst_bus_add_watch(bus, message_func, NULL);
gst_object_unref(bus);
GstCaps *caps = gst_caps_new_empty_simple ("audio/x-raw");
gst_device_monitor_add_filter(monitor, NULL, caps); // "Audio/Source", "Audio/Sink"
gst_caps_unref (caps);
gst_device_monitor_start(monitor);
return monitor;
}
static void gstdev_read_fields(GstDevice *dev, gchar **dev_id, gchar **id_attrib, gchar **dev_descr, gchar **media_role, gchar **dev_class, gchar **icon_name) {
*dev_id = NULL;
*id_attrib = NULL;
*media_role = NULL;
*icon_name = NULL;
gchar *disp_name = gst_device_get_display_name(dev);
// Cut it nicely
*dev_descr = g_strdup(disp_name);
str_cut_nicely(*dev_descr, 39/*to len*/, 25/*minimum len*/);
g_free(disp_name);
*dev_class = gst_device_get_device_class(dev);
GstStructure *dev_props = gst_device_get_properties(dev);
gchar *txt = gst_structure_to_string(dev_props);
g_free(txt);
gint nn = gst_structure_n_fields(dev_props);
for (gint i=0; i < nn; i++) {
const gchar *fname = gst_structure_nth_field_name (dev_props, i);
const gchar *sval;
sval = NULL;
if (gst_structure_get_field_type(dev_props, fname) == G_TYPE_STRING) {
sval = gst_structure_get_string(dev_props, fname);
}
if (g_strrstr0(fname, "object.id")) {
*dev_id = g_strdup(sval);
*id_attrib = g_strdup("path");
} else if (g_strrstr0(fname, "media.class")) {
// *dev_class = g_strdup(sval);
} else if (g_strrstr0(fname, "device.icon_name")) { // <-- Pulseaudio
*icon_name = g_strdup(sval);
} else if (g_strrstr0(fname, "device.icon-name")) { // <-- Pipewire
*icon_name = g_strdup(sval);
} else if (g_strrstr0(fname, "device.form_factor")) { // <-- Pulseaudio
*media_role = g_strdup(sval);
} else if (g_strrstr0(fname, "device.form-factor")) { // <-- Pipewire
*media_role = g_strdup(sval);
}
}
gst_structure_free(dev_props);
// -----------
// Read device id if Pulsesrc
// or read device's path number if Pipewiresrc
GstElement *e = gst_device_create_element(dev, "this_elemnt");
// Id or path number.
// In PulseAudio`s pulsesrc this field is a "device" id.
// See:
// $ pactl list short sources | cut -f2
// $ pactl list | grep -A3 'Source #'
//
// In Pipewire's pipewiresrc this is a "path" number.
// See:
// $ pw-cli dump short Node
GObjectClass *oclass = G_OBJECT_GET_CLASS(e);
guint n = 0;
GParamSpec **props = g_object_class_list_properties(oclass, &n);
GValue val = G_VALUE_INIT;
for (guint i=0; i value_type);
g_object_get_property(G_OBJECT(e), props[i]->name, &val);
// g_print("%i: prop name=%s value=%s\n", i, props[i]->name, g_strdup_value_contents(&val));
//
// prop name=name value="elemntxx"
// prop name=parent value=NULL
// prop name=blocksize value=4096
// prop name=num-buffers value=-1
// prop name=typefind value=FALSE
// prop name=do-timestamp value=FALSE
//
// prop name=path value="80" <-- Pipewire
// or
// name=device value="alsa_output.pci-0000_00_1f.3.analog-stereo" <-- Pulseaudio
//
if (!g_strcmp0(props[i]->name, "role")) {
// *media_role = g_value_dup_string(&val);
} if (!g_strcmp0(props[i]->name, "device")) {
*dev_id = g_value_dup_string(&val);
*id_attrib = g_strdup("device");
} else if (!g_strcmp0(props[i]->name, "path")) {
if (*dev_id == NULL) {
*dev_id = g_value_dup_string(&val);
*id_attrib = g_strdup("path");
}
}
g_value_unset(&val);
}
g_free(props);
gst_object_unref(GST_OBJECT(e));
}
GList *remove_item(GList *list, gchar *dev_id) {
GList *item = g_list_first(list);
while (item) {
DeviceItem *rec = (DeviceItem*)item->data;
if (rec) {
if (rec->id && dev_id && g_strcmp0(rec->id, dev_id) == 0) {
list = g_list_delete_link(list, item);
device_item_free(rec);
return list;
}
}
item = g_list_next(item);
}
// Return unmodified list
return list;
}
static void gstdev_remove_from_list(GstDevice *dev) {
gchar *dev_id = NULL;
gchar *id_attrib = NULL;
gchar *dev_descr = NULL;
gchar *media_role = NULL;
gchar *dev_class = NULL;
gchar *icon_name = NULL;
LOG_DEBUG("Remove device from the list.\n");
G_LOCK(g_source_list);
gstdev_read_fields(dev, &dev_id, &id_attrib, &dev_descr, &media_role, &dev_class, &icon_name);
g_source_list = remove_item(g_source_list, dev_id);
g_free(dev_id);
g_free(id_attrib);
g_free(dev_descr);
g_free(media_role);
g_free(dev_class);
g_free(icon_name);
G_UNLOCK(g_source_list);
}
static void gstdev_add_to_list(GstDevice *dev) {
gchar *dev_descr = NULL;
gchar *dev_id = NULL;
gchar *id_attrib = NULL;
gchar *media_role = NULL;
gchar *dev_class = NULL;
gchar *icon_name = NULL;
LOG_DEBUG("Add new (input or output) device to the list.\n");
G_LOCK(g_source_list);
gstdev_read_fields(dev, &dev_id, &id_attrib, &dev_descr, &media_role, &dev_class, &icon_name);
gboolean add = FALSE;
// Create new DeviceItem
DeviceItem *item = device_item_create(dev_id, dev_descr);
gchar *dev_class_l = g_ascii_strdown(dev_class, -1);
item->dev_class = g_strdup(dev_class_l);
item->id_attrib = g_strdup(id_attrib);
item->media_role = g_strdup(media_role);
// media.class == Audio/Source
// media.class == Video/Source ?and media.role == Camera
// media.class == Audio/Sink
item->type = AUDIO_INPUT;
// Audio/Source
if (g_strrstr(item->dev_class, "audio/source")) {
// Webcam or video camera?
if (g_strrstr0(item->media_role, "cam") || g_strrstr0(item->media_role, "webcam") || g_strrstr0(item->media_role, "camera") || g_strrstr0(item->media_role, "video")) {
// Some suitable icon names
item->icon_name = g_strdup_printf("%s\n%s\n%s\n%s", "camera-web", icon_name, "audio-input-microphone", "microphone.png");
} else {
// Some suitable icon names
item->icon_name = g_strdup_printf("%s\n%s\n%s", icon_name, "audio-input-microphone", "microphone.png");
}
LOG_DEBUG("Add Audio/Source to g_source_list:%s, id_attrib:%s, decr:%s, role:%s class:%s icon:%s\n",
dev_id, id_attrib, dev_descr, media_role, dev_class, item->icon_name);
add = TRUE;
}
// Video/Source (Cameras that capture audio and video)
if (g_strrstr(item->dev_class, "video/source")) {
// Some suitable icon names
item->icon_name = g_strdup_printf("%s\n%s\n%s\n%s", icon_name, "camera-web", "audio-input-microphone", "microphone.png");
LOG_DEBUG("Add Video/Source to g_source_list:%s, id_attrib:%s, decr:%s, role:%s class:%s icon:%s\n",
dev_id, id_attrib, dev_descr, media_role, dev_class, item->icon_name);
add = TRUE;
}
// Audio/Sink (audio card, loudspeakers, mixed output)
if (g_strrstr(item->dev_class, "audio/sink")) {
// Some suitable icon names
item->icon_name = g_strdup_printf("%s\n%s\n%s\n%s", icon_name, "audio-card", "audio-speaker-center", "loudspeaker.png");
LOG_DEBUG("Add Audio/Sink to g_source_list:%s, id_attrib:%s, decr:%s, role:%s class:%s icon:%s\n",
dev_id, id_attrib, dev_descr, media_role, dev_class, item->icon_name);
add = TRUE;
}
if (add) {
g_source_list = g_list_append(g_source_list, item);
} else {
device_item_free(item);
item = NULL;
}
g_free(dev_id);
g_free(id_attrib);
g_free(media_role);
g_free(dev_descr);
g_free(dev_class);
g_free(dev_class_l);
g_free(icon_name);
G_UNLOCK(g_source_list);
}
void gstdev_fix_the_list() {
// Note: Check listing of these commands (in Pulseaudio):
//
// Input devices:
// pactl list | grep -A6 'Source #' | egrep "Name: |Description: "
// pactl list short sources | cut -f2
//
// And sink (output) devices:
// pactl list | grep -A6 'Sink #' | egrep "Name: |Description: "
// pactl list short sinks | cut -f2
// Remove obsolete DeviceItems
// Keep the .monitor device, but delete its audio card listing (its Audio/Sink device). We cannot record from it.
// Below, we delete the _item and keep _item2.
//
// _item->id: alsa_output.pci-0000_00_1f.3.analog-stereo.monitor <-- keep this .monitor device
// _item->description: Monitor of Built-in Audio Analog Stereo
// _item->dev_class: Audio/Source <-- Replace this with "audio/sink" so we can find it later.
// _item2->id: alsa_output.pci-0000_00_1f.3.analog-stereo <-- delete this (cannot record from /Sink)
// _item2->description: Built-in Audio Analog Stereo
// _item2->dev_class: Audio/Sink
G_LOCK(g_source_list);
GList *n = g_list_first(g_source_list);
while (n) {
DeviceItem *item = (DeviceItem*)n->data;
if (g_strrstr0(item->dev_class, "audio/sink")) {
gchar *dev_id = g_strdup_printf("%s.monitor", item->id);
DeviceItem *found_item = audio_sources_find_in_list(g_source_list, dev_id);
if (found_item && found_item != item) {
g_free(found_item->dev_class);
found_item->dev_class = g_strdup("audio/sink");
g_free(found_item->icon_name);
found_item->icon_name = g_strdup(item->icon_name);
// Mark this to be deleted
g_free(item->id);
item->id = NULL;
}
g_free(dev_id);
}
n = g_list_next(n);
}
// Remove NULL items (traverse the list right way)
n = g_list_first(g_source_list);
while (n) {
DeviceItem *item = (DeviceItem*)n->data;
GList *next = g_list_next(n);
if (item->id == NULL) {
g_source_list = g_list_delete_link(g_source_list, n);
device_item_free(item);
}
n = next;
}
G_UNLOCK(g_source_list);
}
static void gstdev_get_devices() {
LOG_DEBUG("Get list of audio input/output devices from GStreamer.\n");
gstdev_clear_lists();
// Set up device monitor
if (!GST_IS_DEVICE_MONITOR(g_dev_monitor)) {
g_dev_monitor = setup_raw_audio_source_device_monitor();
}
GList *list = gst_device_monitor_get_devices(g_dev_monitor);
// Ref: http://code.metager.de/source/xref/freedesktop/gstreamer/gstreamer/gst/gstdevice.c
GList *n = g_list_first(list);
while (n) {
GstDevice *dev = (GstDevice*)n->data;
gstdev_add_to_list(dev);
n = g_list_next(n);
}
g_list_free_full(list, (GDestroyNotify)gst_object_unref);
// Fix the list if pulseaudio
gstdev_fix_the_list();
}