master
c 308 lines 8.48 KB
Raw
1 /*
2 * Standalone VNC server connecting to QEMU via D-Bus display interface.
3 * Audio support. Only one audio stream is tracked.
4 * Mixing/resampling to be added, if needed.
5 *
6 * Copyright (C) 2026 Red Hat, Inc.
7 *
8 * SPDX-License-Identifier: GPL-2.0-or-later
9 */
10
11 #include "qemu/osdep.h"
12
13 #include "qemu/audio.h"
14 #include "qemu/audio-capture.h"
15 #include "qemu/sockets.h"
16 #include "qemu/error-report.h"
17 #include "ui/dbus-display1.h"
18 #include "trace.h"
19 #include "qemu-vnc.h"
20
21 struct CaptureVoiceOut {
22 struct audsettings as;
23 struct audio_capture_ops ops;
24 void *opaque;
25 QLIST_ENTRY(CaptureVoiceOut) entries;
26 };
27
28 typedef struct AudioOut {
29 guint64 id;
30 struct audsettings as;
31 } AudioOut;
32
33 static QLIST_HEAD(, CaptureVoiceOut) capture_list =
34 QLIST_HEAD_INITIALIZER(capture_list);
35 static GDBusConnection *audio_listener_conn;
36 static AudioOut audio_out;
37
38 static bool audsettings_eq(const struct audsettings *a,
39 const struct audsettings *b)
40 {
41 return a->freq == b->freq &&
42 a->nchannels == b->nchannels &&
43 a->fmt == b->fmt &&
44 a->big_endian == b->big_endian;
45 }
46
47 static gboolean
48 on_audio_out_init(QemuDBusDisplay1AudioOutListener *listener,
49 GDBusMethodInvocation *invocation,
50 guint64 id, guchar bits, gboolean is_signed,
51 gboolean is_float, guint freq, guchar nchannels,
52 guint bytes_per_frame, guint bytes_per_second,
53 gboolean be, gpointer user_data)
54 {
55 AudioFormat fmt;
56
57 switch (bits) {
58 case 8:
59 fmt = is_signed ? AUDIO_FORMAT_S8 : AUDIO_FORMAT_U8;
60 break;
61 case 16:
62 fmt = is_signed ? AUDIO_FORMAT_S16 : AUDIO_FORMAT_U16;
63 break;
64 case 32:
65 fmt = is_float ? AUDIO_FORMAT_F32 :
66 is_signed ? AUDIO_FORMAT_S32 : AUDIO_FORMAT_U32;
67 break;
68 default:
69 g_return_val_if_reached(DBUS_METHOD_INVOCATION_HANDLED);
70 }
71
72 struct audsettings as = {
73 .freq = freq,
74 .nchannels = nchannels,
75 .fmt = fmt,
76 .big_endian = be,
77 };
78 audio_out = (AudioOut) {
79 .id = id,
80 .as = as,
81 };
82
83 trace_qemu_vnc_audio_out_init(id, freq, nchannels, bits);
84
85 qemu_dbus_display1_audio_out_listener_complete_init(
86 listener, invocation);
87 return DBUS_METHOD_INVOCATION_HANDLED;
88 }
89
90 static gboolean
91 on_audio_out_fini(QemuDBusDisplay1AudioOutListener *listener,
92 GDBusMethodInvocation *invocation,
93 guint64 id, gpointer user_data)
94 {
95 trace_qemu_vnc_audio_out_fini(id);
96
97 qemu_dbus_display1_audio_out_listener_complete_fini(
98 listener, invocation);
99 return DBUS_METHOD_INVOCATION_HANDLED;
100 }
101
102 static gboolean
103 on_audio_out_set_enabled(QemuDBusDisplay1AudioOutListener *listener,
104 GDBusMethodInvocation *invocation,
105 guint64 id, gboolean enabled,
106 gpointer user_data)
107 {
108 CaptureVoiceOut *cap;
109
110 trace_qemu_vnc_audio_out_set_enabled(id, enabled);
111
112 if (id == audio_out.id) {
113 QLIST_FOREACH(cap, &capture_list, entries) {
114 cap->ops.notify(cap->opaque,
115 enabled ? AUD_CNOTIFY_ENABLE
116 : AUD_CNOTIFY_DISABLE);
117 }
118 }
119
120 qemu_dbus_display1_audio_out_listener_complete_set_enabled(
121 listener, invocation);
122 return DBUS_METHOD_INVOCATION_HANDLED;
123 }
124
125 static gboolean
126 on_audio_out_set_volume(QemuDBusDisplay1AudioOutListener *listener,
127 GDBusMethodInvocation *invocation,
128 guint64 id, gboolean mute,
129 GVariant *volume, gpointer user_data)
130 {
131 qemu_dbus_display1_audio_out_listener_complete_set_volume(
132 listener, invocation);
133 return DBUS_METHOD_INVOCATION_HANDLED;
134 }
135
136 static gboolean
137 on_audio_out_write(QemuDBusDisplay1AudioOutListener *listener,
138 GDBusMethodInvocation *invocation,
139 guint64 id, GVariant *data,
140 gpointer user_data)
141 {
142 CaptureVoiceOut *cap;
143 gsize size;
144 const void *buf;
145
146 if (id == audio_out.id) {
147 buf = g_variant_get_fixed_array(data, &size, 1);
148
149 trace_qemu_vnc_audio_out_write(id, size);
150
151 QLIST_FOREACH(cap, &capture_list, entries) {
152 /* we don't handle audio resampling/format conversion */
153 if (audsettings_eq(&cap->as, &audio_out.as)) {
154 cap->ops.capture(cap->opaque, buf, size);
155 }
156 }
157 }
158
159 qemu_dbus_display1_audio_out_listener_complete_write(
160 listener, invocation);
161 return DBUS_METHOD_INVOCATION_HANDLED;
162 }
163
164 CaptureVoiceOut *audio_be_add_capture(
165 AudioBackend *be,
166 const struct audsettings *as,
167 const struct audio_capture_ops *ops,
168 void *opaque)
169 {
170 CaptureVoiceOut *cap;
171
172 if (!audio_listener_conn) {
173 return NULL;
174 }
175
176 cap = g_new0(CaptureVoiceOut, 1);
177 cap->ops = *ops;
178 cap->opaque = opaque;
179 cap->as = *as;
180 QLIST_INSERT_HEAD(&capture_list, cap, entries);
181
182 return cap;
183 }
184
185 void audio_be_del_capture(
186 AudioBackend *be,
187 CaptureVoiceOut *cap,
188 void *cb_opaque)
189 {
190 if (!cap) {
191 return;
192 }
193
194 cap->ops.destroy(cap->opaque);
195 QLIST_REMOVE(cap, entries);
196 g_free(cap);
197 }
198
199 /*
200 * Dummy audio backend — the VNC server only needs a non-NULL pointer
201 * so that audio capture registration doesn't bail out. The pointer
202 * is never dereferenced by our code (audio_be_add_capture ignores it).
203 */
204 static AudioBackend dummy_audio_be;
205
206 AudioBackend *audio_get_default_audio_be(Error **errp)
207 {
208 return &dummy_audio_be;
209 }
210
211 AudioBackend *audio_be_by_name(const char *name, Error **errp)
212 {
213 return NULL;
214 }
215
216 static void
217 on_register_audio_listener_finished(GObject *source_object,
218 GAsyncResult *res,
219 gpointer user_data)
220 {
221 GThread *thread = user_data;
222 g_autoptr(GError) err = NULL;
223 g_autoptr(GDBusObjectSkeleton) obj = NULL;
224 GDBusObjectManagerServer *server;
225 QemuDBusDisplay1AudioOutListener *audio_skel;
226
227 qemu_dbus_display1_audio_call_register_out_listener_finish(
228 QEMU_DBUS_DISPLAY1_AUDIO(source_object),
229 NULL, res, &err);
230
231 if (err) {
232 error_report("RegisterOutListener failed: %s", err->message);
233 g_thread_join(thread);
234 return;
235 }
236
237 audio_listener_conn = g_thread_join(thread);
238 if (!audio_listener_conn) {
239 return;
240 }
241
242 server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT);
243 obj = g_dbus_object_skeleton_new(
244 DBUS_DISPLAY1_ROOT "/AudioOutListener");
245
246 audio_skel = qemu_dbus_display1_audio_out_listener_skeleton_new();
247 g_object_connect(audio_skel,
248 "signal::handle-init",
249 on_audio_out_init, NULL,
250 "signal::handle-fini",
251 on_audio_out_fini, NULL,
252 "signal::handle-set-enabled",
253 on_audio_out_set_enabled, NULL,
254 "signal::handle-set-volume",
255 on_audio_out_set_volume, NULL,
256 "signal::handle-write",
257 on_audio_out_write, NULL,
258 NULL);
259 g_dbus_object_skeleton_add_interface(
260 obj, G_DBUS_INTERFACE_SKELETON(audio_skel));
261
262 g_dbus_object_manager_server_export(server, obj);
263 g_dbus_object_manager_server_set_connection(
264 server, audio_listener_conn);
265
266 g_dbus_connection_start_message_processing(audio_listener_conn);
267 }
268
269 void audio_setup(GDBusObjectManager *manager)
270 {
271 g_autoptr(GError) err = NULL;
272 g_autoptr(GUnixFDList) fd_list = NULL;
273 g_autoptr(GDBusInterface) iface = NULL;
274 GThread *thread;
275 int pair[2];
276 int idx;
277
278 iface = g_dbus_object_manager_get_interface(
279 manager, DBUS_DISPLAY1_ROOT "/Audio",
280 "org.qemu.Display1.Audio");
281 if (!iface) {
282 return;
283 }
284
285 if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {
286 error_report("audio socketpair failed: %s", strerror(errno));
287 return;
288 }
289
290 fd_list = g_unix_fd_list_new();
291 idx = g_unix_fd_list_append(fd_list, pair[1], &err);
292 close(pair[1]);
293 if (idx < 0) {
294 close(pair[0]);
295 error_report("Failed to append fd: %s", err->message);
296 return;
297 }
298
299 thread = p2p_dbus_thread_new(pair[0]);
300
301 qemu_dbus_display1_audio_call_register_out_listener(
302 QEMU_DBUS_DISPLAY1_AUDIO(iface),
303 g_variant_new_handle(idx),
304 G_DBUS_CALL_FLAGS_NONE, -1,
305 fd_list, NULL,
306 on_register_audio_listener_finished,
307 thread);
308 }