blob: 89921d1fad80e562cf41e860a2a12e7d172c4ad7 [file] [edit]
/*
* Standalone VNC server connecting to QEMU via D-Bus display interface.
* Audio support. Only one audio stream is tracked.
* Mixing/resampling to be added, if needed.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qemu/audio.h"
#include "qemu/audio-capture.h"
#include "qemu/sockets.h"
#include "qemu/error-report.h"
#include "ui/dbus-display1.h"
#include "trace.h"
#include "qemu-vnc.h"
struct CaptureVoiceOut {
struct audsettings as;
struct audio_capture_ops ops;
void *opaque;
QLIST_ENTRY(CaptureVoiceOut) entries;
};
typedef struct AudioOut {
guint64 id;
struct audsettings as;
} AudioOut;
static QLIST_HEAD(, CaptureVoiceOut) capture_list =
QLIST_HEAD_INITIALIZER(capture_list);
static GDBusConnection *audio_listener_conn;
static AudioOut audio_out;
static bool audsettings_eq(const struct audsettings *a,
const struct audsettings *b)
{
return a->freq == b->freq &&
a->nchannels == b->nchannels &&
a->fmt == b->fmt &&
a->big_endian == b->big_endian;
}
static gboolean
on_audio_out_init(QemuDBusDisplay1AudioOutListener *listener,
GDBusMethodInvocation *invocation,
guint64 id, guchar bits, gboolean is_signed,
gboolean is_float, guint freq, guchar nchannels,
guint bytes_per_frame, guint bytes_per_second,
gboolean be, gpointer user_data)
{
AudioFormat fmt;
switch (bits) {
case 8:
fmt = is_signed ? AUDIO_FORMAT_S8 : AUDIO_FORMAT_U8;
break;
case 16:
fmt = is_signed ? AUDIO_FORMAT_S16 : AUDIO_FORMAT_U16;
break;
case 32:
fmt = is_float ? AUDIO_FORMAT_F32 :
is_signed ? AUDIO_FORMAT_S32 : AUDIO_FORMAT_U32;
break;
default:
g_return_val_if_reached(DBUS_METHOD_INVOCATION_HANDLED);
}
struct audsettings as = {
.freq = freq,
.nchannels = nchannels,
.fmt = fmt,
.big_endian = be,
};
audio_out = (AudioOut) {
.id = id,
.as = as,
};
trace_qemu_vnc_audio_out_init(id, freq, nchannels, bits);
qemu_dbus_display1_audio_out_listener_complete_init(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_audio_out_fini(QemuDBusDisplay1AudioOutListener *listener,
GDBusMethodInvocation *invocation,
guint64 id, gpointer user_data)
{
trace_qemu_vnc_audio_out_fini(id);
qemu_dbus_display1_audio_out_listener_complete_fini(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_audio_out_set_enabled(QemuDBusDisplay1AudioOutListener *listener,
GDBusMethodInvocation *invocation,
guint64 id, gboolean enabled,
gpointer user_data)
{
CaptureVoiceOut *cap;
trace_qemu_vnc_audio_out_set_enabled(id, enabled);
if (id == audio_out.id) {
QLIST_FOREACH(cap, &capture_list, entries) {
cap->ops.notify(cap->opaque,
enabled ? AUD_CNOTIFY_ENABLE
: AUD_CNOTIFY_DISABLE);
}
}
qemu_dbus_display1_audio_out_listener_complete_set_enabled(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_audio_out_set_volume(QemuDBusDisplay1AudioOutListener *listener,
GDBusMethodInvocation *invocation,
guint64 id, gboolean mute,
GVariant *volume, gpointer user_data)
{
qemu_dbus_display1_audio_out_listener_complete_set_volume(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_audio_out_write(QemuDBusDisplay1AudioOutListener *listener,
GDBusMethodInvocation *invocation,
guint64 id, GVariant *data,
gpointer user_data)
{
CaptureVoiceOut *cap;
gsize size;
const void *buf;
if (id == audio_out.id) {
buf = g_variant_get_fixed_array(data, &size, 1);
trace_qemu_vnc_audio_out_write(id, size);
QLIST_FOREACH(cap, &capture_list, entries) {
/* we don't handle audio resampling/format conversion */
if (audsettings_eq(&cap->as, &audio_out.as)) {
cap->ops.capture(cap->opaque, buf, size);
}
}
}
qemu_dbus_display1_audio_out_listener_complete_write(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
CaptureVoiceOut *audio_be_add_capture(
AudioBackend *be,
const struct audsettings *as,
const struct audio_capture_ops *ops,
void *opaque)
{
CaptureVoiceOut *cap;
if (!audio_listener_conn) {
return NULL;
}
cap = g_new0(CaptureVoiceOut, 1);
cap->ops = *ops;
cap->opaque = opaque;
cap->as = *as;
QLIST_INSERT_HEAD(&capture_list, cap, entries);
return cap;
}
void audio_be_del_capture(
AudioBackend *be,
CaptureVoiceOut *cap,
void *cb_opaque)
{
if (!cap) {
return;
}
cap->ops.destroy(cap->opaque);
QLIST_REMOVE(cap, entries);
g_free(cap);
}
/*
* Dummy audio backend — the VNC server only needs a non-NULL pointer
* so that audio capture registration doesn't bail out. The pointer
* is never dereferenced by our code (audio_be_add_capture ignores it).
*/
static AudioBackend dummy_audio_be;
AudioBackend *audio_get_default_audio_be(Error **errp)
{
return &dummy_audio_be;
}
AudioBackend *audio_be_by_name(const char *name, Error **errp)
{
return NULL;
}
static void
on_register_audio_listener_finished(GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
GThread *thread = user_data;
g_autoptr(GError) err = NULL;
g_autoptr(GDBusObjectSkeleton) obj = NULL;
GDBusObjectManagerServer *server;
QemuDBusDisplay1AudioOutListener *audio_skel;
qemu_dbus_display1_audio_call_register_out_listener_finish(
QEMU_DBUS_DISPLAY1_AUDIO(source_object),
NULL, res, &err);
if (err) {
error_report("RegisterOutListener failed: %s", err->message);
g_thread_join(thread);
return;
}
audio_listener_conn = g_thread_join(thread);
if (!audio_listener_conn) {
return;
}
server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT);
obj = g_dbus_object_skeleton_new(
DBUS_DISPLAY1_ROOT "/AudioOutListener");
audio_skel = qemu_dbus_display1_audio_out_listener_skeleton_new();
g_object_connect(audio_skel,
"signal::handle-init",
on_audio_out_init, NULL,
"signal::handle-fini",
on_audio_out_fini, NULL,
"signal::handle-set-enabled",
on_audio_out_set_enabled, NULL,
"signal::handle-set-volume",
on_audio_out_set_volume, NULL,
"signal::handle-write",
on_audio_out_write, NULL,
NULL);
g_dbus_object_skeleton_add_interface(
obj, G_DBUS_INTERFACE_SKELETON(audio_skel));
g_dbus_object_manager_server_export(server, obj);
g_dbus_object_manager_server_set_connection(
server, audio_listener_conn);
g_dbus_connection_start_message_processing(audio_listener_conn);
}
void audio_setup(GDBusObjectManager *manager)
{
g_autoptr(GError) err = NULL;
g_autoptr(GUnixFDList) fd_list = NULL;
g_autoptr(GDBusInterface) iface = NULL;
GThread *thread;
int pair[2];
int idx;
iface = g_dbus_object_manager_get_interface(
manager, DBUS_DISPLAY1_ROOT "/Audio",
"org.qemu.Display1.Audio");
if (!iface) {
return;
}
if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {
error_report("audio socketpair failed: %s", strerror(errno));
return;
}
fd_list = g_unix_fd_list_new();
idx = g_unix_fd_list_append(fd_list, pair[1], &err);
close(pair[1]);
if (idx < 0) {
close(pair[0]);
error_report("Failed to append fd: %s", err->message);
return;
}
thread = p2p_dbus_thread_new(pair[0]);
qemu_dbus_display1_audio_call_register_out_listener(
QEMU_DBUS_DISPLAY1_AUDIO(iface),
g_variant_new_handle(idx),
G_DBUS_CALL_FLAGS_NONE, -1,
fd_list, NULL,
on_register_audio_listener_finished,
thread);
}