/* * 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(); }