feat: create virtual devices (+ Add Device dropdown)
Backend:
- POST /api/create-null-sink {name} - loads null-sink module
- POST /api/create-loopback {name} - loads loopback module
- POST /api/unload-module {module_id} - unloads a module
- Fixed double-proxy-destroy crash in GraphEngine
- Graceful failure when module not available (no crash)
Frontend:
- + Add Device button in toolbar with dropdown menu
- Null Sink option (creates virtual audio output)
- Loopback Device option (creates paired input+output)
- Dropdown closes on outside click
Note: null-sink requires libpipewire-module-null-sink to be installed.
Loopback works on all PipeWire installations.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
#include "graph_engine.h"
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <pipewire/impl-module.h>
|
||||
#include <pipewire/global.h>
|
||||
#include <spa/utils/result.h>
|
||||
#include <spa/utils/list.h>
|
||||
#include <spa/param/props.h>
|
||||
@@ -45,12 +47,19 @@ static void remove_pending(GraphEngine::Object *obj) {
|
||||
// ============================================================================
|
||||
|
||||
static void destroy_proxy(GraphEngine::Object *obj) {
|
||||
if (obj->proxy) {
|
||||
pw_proxy_destroy((pw_proxy*)obj->proxy);
|
||||
pw_proxy *proxy = (pw_proxy*)obj->proxy;
|
||||
if (proxy) {
|
||||
obj->proxy = nullptr;
|
||||
pw_proxy_destroy(proxy);
|
||||
}
|
||||
if (obj->object_listener.link.next) {
|
||||
spa_hook_remove(&obj->object_listener);
|
||||
spa_zero(obj->object_listener);
|
||||
}
|
||||
if (obj->proxy_listener.link.next) {
|
||||
spa_hook_remove(&obj->proxy_listener);
|
||||
spa_zero(obj->proxy_listener);
|
||||
}
|
||||
spa_hook_remove(&obj->object_listener);
|
||||
spa_hook_remove(&obj->proxy_listener);
|
||||
remove_pending(obj);
|
||||
if (obj->info && obj->destroy_info) {
|
||||
obj->destroy_info(obj->info);
|
||||
@@ -853,4 +862,40 @@ bool GraphEngine::setNodeMute(uint32_t node_id, bool mute) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Module loading (virtual devices)
|
||||
// ============================================================================
|
||||
|
||||
uint32_t GraphEngine::loadModule(const char *name, const char *args) {
|
||||
if (!m_pw.loop || !m_pw.context) return 0;
|
||||
|
||||
pw_thread_loop_lock(m_pw.loop);
|
||||
|
||||
struct pw_impl_module *mod = pw_context_load_module(m_pw.context, name, args, nullptr);
|
||||
uint32_t id = 0;
|
||||
if (!mod) {
|
||||
fprintf(stderr, "pwweb: failed to load module %s\n", name);
|
||||
} else {
|
||||
struct pw_global *global = pw_impl_module_get_global(mod);
|
||||
if (global) {
|
||||
id = pw_global_get_id(global);
|
||||
}
|
||||
fprintf(stderr, "pwweb: loaded module %s (id=%u)\n", name, id);
|
||||
}
|
||||
|
||||
pw_thread_loop_unlock(m_pw.loop);
|
||||
return id;
|
||||
}
|
||||
|
||||
bool GraphEngine::unloadModule(uint32_t module_id) {
|
||||
if (!m_pw.loop || !m_pw.registry) return false;
|
||||
|
||||
pw_thread_loop_lock(m_pw.loop);
|
||||
pw_registry_destroy(m_pw.registry, module_id);
|
||||
pw_thread_loop_unlock(m_pw.loop);
|
||||
|
||||
fprintf(stderr, "pwweb: unloaded module id=%u\n", module_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// end of graph_engine.cpp
|
||||
|
||||
Reference in New Issue
Block a user