[PATCH 0/3] qemu: Introduce pipewire audio backend

You'd expect to see src/qemu/qemu_capabilities.c changed, wouldn't you. Well, there's no way to query for supported audio backends, see: https://gitlab.com/libvirt/libvirt/-/issues/473 Michal Prívozník (3): conf: Introduce pipewire audio backend qemu: Generate cmd line for pipewire audio backend NEWS: Document pipewire audio backend NEWS.rst | 10 +++ docs/formatdomain.rst | 35 +++++++++- src/conf/domain_conf.c | 70 +++++++++++++++++++ src/conf/domain_conf.h | 12 ++++ src/conf/schemas/domaincommon.rng | 37 ++++++++++ src/qemu/qemu_command.c | 63 +++++++++++++++++ src/qemu/qemu_validate.c | 1 + .../audio-many-backends.x86_64-latest.args | 2 + .../qemuxml2argvdata/audio-many-backends.xml | 1 + .../audio-pipewire-best.x86_64-latest.args | 36 ++++++++++ .../qemuxml2argvdata/audio-pipewire-best.xml | 43 ++++++++++++ .../audio-pipewire-full.x86_64-latest.args | 36 ++++++++++ .../qemuxml2argvdata/audio-pipewire-full.xml | 43 ++++++++++++ .../audio-pipewire-minimal.x86_64-latest.args | 36 ++++++++++ .../audio-pipewire-minimal.xml | 36 ++++++++++ tests/qemuxml2argvtest.c | 12 ++++ .../audio-pipewire-best.xml | 43 ++++++++++++ .../audio-pipewire-full.xml | 43 ++++++++++++ .../audio-pipewire-minimal.xml | 36 ++++++++++ tests/qemuxml2xmltest.c | 3 + 20 files changed, 596 insertions(+), 2 deletions(-) create mode 100644 tests/qemuxml2argvdata/audio-pipewire-best.x86_64-latest.args create mode 100644 tests/qemuxml2argvdata/audio-pipewire-best.xml create mode 100644 tests/qemuxml2argvdata/audio-pipewire-full.x86_64-latest.args create mode 100644 tests/qemuxml2argvdata/audio-pipewire-full.xml create mode 100644 tests/qemuxml2argvdata/audio-pipewire-minimal.x86_64-latest.args create mode 100644 tests/qemuxml2argvdata/audio-pipewire-minimal.xml create mode 100644 tests/qemuxml2xmloutdata/audio-pipewire-best.xml create mode 100644 tests/qemuxml2xmloutdata/audio-pipewire-full.xml create mode 100644 tests/qemuxml2xmloutdata/audio-pipewire-minimal.xml -- 2.39.3

QEMU gained support for PipeWire audio backend (see QEMU commit of v8.0.0-403-gc2d3d1c294). Its configuration knobs are basically the same as pulseaudio's, except for PA's server name. Therefore, a lot of code is copied over from pulseadio and fixed by s/Pulse/Pipewire/ or s/pulseaudio/pipewire/. Signed-off-by: Michal Privoznik <mprivozn@redhat.com> --- docs/formatdomain.rst | 35 +++++++++- src/conf/domain_conf.c | 70 +++++++++++++++++++ src/conf/domain_conf.h | 12 ++++ src/conf/schemas/domaincommon.rng | 37 ++++++++++ src/qemu/qemu_command.c | 2 + src/qemu/qemu_validate.c | 1 + .../qemuxml2argvdata/audio-pipewire-best.xml | 43 ++++++++++++ .../qemuxml2argvdata/audio-pipewire-full.xml | 43 ++++++++++++ .../audio-pipewire-minimal.xml | 36 ++++++++++ .../audio-pipewire-best.xml | 43 ++++++++++++ .../audio-pipewire-full.xml | 43 ++++++++++++ .../audio-pipewire-minimal.xml | 36 ++++++++++ tests/qemuxml2xmltest.c | 3 + 13 files changed, 402 insertions(+), 2 deletions(-) create mode 100644 tests/qemuxml2argvdata/audio-pipewire-best.xml create mode 100644 tests/qemuxml2argvdata/audio-pipewire-full.xml create mode 100644 tests/qemuxml2argvdata/audio-pipewire-minimal.xml create mode 100644 tests/qemuxml2xmloutdata/audio-pipewire-best.xml create mode 100644 tests/qemuxml2xmloutdata/audio-pipewire-full.xml create mode 100644 tests/qemuxml2xmloutdata/audio-pipewire-minimal.xml diff --git a/docs/formatdomain.rst b/docs/formatdomain.rst index 99383e725c..1ec68c4776 100644 --- a/docs/formatdomain.rst +++ b/docs/formatdomain.rst @@ -7270,7 +7270,8 @@ to the guest sound device. ``type`` The required ``type`` attribute specifies audio backend type. Currently, the supported values are ``none``, ``alsa``, ``coreaudio``, - ``dbus``, ``jack``, ``oss``, ``pulseaudio``, ``sdl``, ``spice``, ``file``. + ``dbus``, ``jack``, ``oss``, ``pipewire``, ``pulseaudio``, ``sdl``, + ``spice``, ``file``. ``id`` Integer id of the audio device. Must be greater than 0. @@ -7354,7 +7355,7 @@ following environment variables: * ``QEMU_AUDIO_DRV`` Valid values are ``pa``, ``none``, ``alsa``, ``coreaudio``, ``jack``, ``oss``, - ``sdl``, ``spice`` or ``wav``. + ``pipewire``, ``sdl``, ``spice`` or ``wav``. None audio backend ^^^^^^^^^^^^^^^^^^ @@ -7503,6 +7504,36 @@ and ``<output>`` elements :since:`Since 6.7.0, bhyve; Since 7.2.0, qemu` +PipeWire audio backend +^^^^^^^^^^^^^^^^^^^^^^ + +The ``pipewire`` audio backend delegates to a PipeWire daemon audio input and +output. + +The following additional attributes are permitted on the ``<input/>`` and +``<output/>`` elements: + +* ``name`` + + The sink/source name to use + +* ``streamName`` + + The name to identify the stream associated with the VM + +* ``latency`` + + Desired latency for the server to target in microseconds + +:: + + <audio id="1" type="pipewire"> + <input name="fish" streamName="food" latency="100"/> + <output name="fish" streamName="food" latency="200"/> + </audio> + +:since:`Since 9.4.0, qemu` + PulseAudio audio backend ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/conf/domain_conf.c b/src/conf/domain_conf.c index 6a864a8db9..62484631cc 100644 --- a/src/conf/domain_conf.c +++ b/src/conf/domain_conf.c @@ -788,6 +788,7 @@ VIR_ENUM_IMPL(virDomainAudioType, "spice", "file", "dbus", + "pipewire", ); VIR_ENUM_IMPL(virDomainAudioSDLDriver, @@ -3226,6 +3227,13 @@ virDomainAudioIOPulseAudioFree(virDomainAudioIOPulseAudio *def) g_free(def->streamName); } +static void +virDomainAudioIOPipewireAudioFree(virDomainAudioIOPipewireAudio *def) +{ + g_free(def->name); + g_free(def->streamName); +} + void virDomainAudioDefFree(virDomainAudioDef *def) { @@ -3270,6 +3278,11 @@ virDomainAudioDefFree(virDomainAudioDef *def) g_free(def->backend.file.path); break; + case VIR_DOMAIN_AUDIO_TYPE_PIPEWIRE: + virDomainAudioIOPipewireAudioFree(&def->backend.pipewire.input); + virDomainAudioIOPipewireAudioFree(&def->backend.pipewire.output); + break; + case VIR_DOMAIN_AUDIO_TYPE_DBUS: case VIR_DOMAIN_AUDIO_TYPE_LAST: break; @@ -11897,6 +11910,21 @@ virDomainAudioSDLParse(virDomainAudioIOSDL *def, } +static int +virDomainAudioPipewireAudioParse(virDomainAudioIOPipewireAudio *def, + xmlNodePtr node) +{ + def->name = virXMLPropString(node, "name"); + def->streamName = virXMLPropString(node, "streamName"); + + if (virXMLPropUInt(node, "latency", 10, VIR_XML_PROP_NONE, + &def->latency) < 0) + return -1; + + return 0; +} + + static virDomainAudioDef * virDomainAudioDefParseXML(virDomainXMLOption *xmlopt G_GNUC_UNUSED, xmlNodePtr node, @@ -12027,6 +12055,15 @@ virDomainAudioDefParseXML(virDomainXMLOption *xmlopt G_GNUC_UNUSED, case VIR_DOMAIN_AUDIO_TYPE_DBUS: break; + case VIR_DOMAIN_AUDIO_TYPE_PIPEWIRE: + if (inputNode && + virDomainAudioPipewireAudioParse(&def->backend.pipewire.input, inputNode) < 0) + goto error; + if (outputNode && + virDomainAudioPipewireAudioParse(&def->backend.pipewire.output, outputNode) < 0) + goto error; + break; + case VIR_DOMAIN_AUDIO_TYPE_LAST: default: virReportEnumRangeError(virDomainAudioType, def->type); @@ -24679,6 +24716,18 @@ virDomainAudioSDLFormat(virDomainAudioIOSDL *def, } +static void +virDomainAudioPipewireAudioFormat(virDomainAudioIOPipewireAudio *def, + virBuffer *buf) +{ + virBufferEscapeString(buf, " name='%s'", def->name); + virBufferEscapeString(buf, " streamName='%s'", def->streamName); + if (def->latency) + virBufferAsprintf(buf, " latency='%u'", def->latency); + +} + + static int virDomainAudioDefFormat(virBuffer *buf, virDomainAudioDef *def) @@ -24761,6 +24810,11 @@ virDomainAudioDefFormat(virBuffer *buf, case VIR_DOMAIN_AUDIO_TYPE_DBUS: break; + case VIR_DOMAIN_AUDIO_TYPE_PIPEWIRE: + virDomainAudioPipewireAudioFormat(&def->backend.pipewire.input, &inputBuf); + virDomainAudioPipewireAudioFormat(&def->backend.pipewire.output, &outputBuf); + break; + case VIR_DOMAIN_AUDIO_TYPE_LAST: default: virReportEnumRangeError(virDomainAudioType, def->type); @@ -29065,6 +29119,16 @@ virDomainAudioIOSDLIsEqual(virDomainAudioIOSDL *this, } +static bool +virDomainAudioIOPipewireAudioIsEqual(virDomainAudioIOPipewireAudio *this, + virDomainAudioIOPipewireAudio *that) +{ + return STREQ_NULLABLE(this->name, that->name) && + STREQ_NULLABLE(this->streamName, that->streamName) && + this->latency == that->latency; +} + + static bool virDomainAudioBackendIsEqual(virDomainAudioDef *this, virDomainAudioDef *that) @@ -29125,6 +29189,12 @@ virDomainAudioBackendIsEqual(virDomainAudioDef *this, case VIR_DOMAIN_AUDIO_TYPE_FILE: return STREQ_NULLABLE(this->backend.file.path, that->backend.file.path); + case VIR_DOMAIN_AUDIO_TYPE_PIPEWIRE: + return virDomainAudioIOPipewireAudioIsEqual(&this->backend.pipewire.input, + &that->backend.pipewire.input) && + virDomainAudioIOPipewireAudioIsEqual(&this->backend.pipewire.output, + &that->backend.pipewire.output); + case VIR_DOMAIN_AUDIO_TYPE_DBUS: case VIR_DOMAIN_AUDIO_TYPE_LAST: default: diff --git a/src/conf/domain_conf.h b/src/conf/domain_conf.h index c1cb2ed69d..7abac25ae1 100644 --- a/src/conf/domain_conf.h +++ b/src/conf/domain_conf.h @@ -1614,6 +1614,7 @@ typedef enum { VIR_DOMAIN_AUDIO_TYPE_SPICE, VIR_DOMAIN_AUDIO_TYPE_FILE, VIR_DOMAIN_AUDIO_TYPE_DBUS, + VIR_DOMAIN_AUDIO_TYPE_PIPEWIRE, VIR_DOMAIN_AUDIO_TYPE_LAST } virDomainAudioType; @@ -1689,6 +1690,13 @@ struct _virDomainAudioIOSDL { unsigned int bufferCount; }; +typedef struct _virDomainAudioIOPipewireAudio virDomainAudioIOPipewireAudio; +struct _virDomainAudioIOPipewireAudio { + char *name; + char *streamName; + unsigned int latency; +}; + struct _virDomainAudioDef { virDomainAudioType type; @@ -1732,6 +1740,10 @@ struct _virDomainAudioDef { struct { char *path; } file; + struct { + virDomainAudioIOPipewireAudio input; + virDomainAudioIOPipewireAudio output; + } pipewire; } backend; }; diff --git a/src/conf/schemas/domaincommon.rng b/src/conf/schemas/domaincommon.rng index f8c7b6a648..793544e15e 100644 --- a/src/conf/schemas/domaincommon.rng +++ b/src/conf/schemas/domaincommon.rng @@ -5121,6 +5121,26 @@ <ref name="audiocommonchild"/> </define> + <define name="audiopipewire"> + <ref name="audiocommonattr"/> + <optional> + <attribute name="name"> + <data type="string"/> + </attribute> + </optional> + <optional> + <attribute name="streamName"> + <data type="string"/> + </attribute> + </optional> + <optional> + <attribute name="latency"> + <ref name="uint32"/> + </attribute> + </optional> + <ref name="audiocommonchild"/> + </define> + <define name="audiopulseaudio"> <ref name="audiocommonattr"/> <optional> @@ -5296,6 +5316,23 @@ </optional> </interleave> </group> + <group> + <attribute name="type"> + <value>pipewire</value> + </attribute> + <interleave> + <optional> + <element name="input"> + <ref name="audiopipewire"/> + </element> + </optional> + <optional> + <element name="output"> + <ref name="audiopipewire"/> + </element> + </optional> + </interleave> + </group> <group> <attribute name="type"> <value>pulseaudio</value> diff --git a/src/qemu/qemu_command.c b/src/qemu/qemu_command.c index 2a6d9408f6..3a08cac870 100644 --- a/src/qemu/qemu_command.c +++ b/src/qemu/qemu_command.c @@ -142,6 +142,7 @@ qemuAudioDriverTypeToString(virDomainAudioType type) case VIR_DOMAIN_AUDIO_TYPE_SDL: case VIR_DOMAIN_AUDIO_TYPE_SPICE: case VIR_DOMAIN_AUDIO_TYPE_DBUS: + case VIR_DOMAIN_AUDIO_TYPE_PIPEWIRE: case VIR_DOMAIN_AUDIO_TYPE_LAST: break; } @@ -7887,6 +7888,7 @@ qemuBuildAudioCommandLineArg(virCommand *cmd, case VIR_DOMAIN_AUDIO_TYPE_DBUS: break; + case VIR_DOMAIN_AUDIO_TYPE_PIPEWIRE: case VIR_DOMAIN_AUDIO_TYPE_LAST: default: virReportEnumRangeError(virDomainAudioType, def->type); diff --git a/src/qemu/qemu_validate.c b/src/qemu/qemu_validate.c index da4b9a3b35..90e2517649 100644 --- a/src/qemu/qemu_validate.c +++ b/src/qemu/qemu_validate.c @@ -4476,6 +4476,7 @@ qemuValidateDomainDeviceDefAudio(virDomainAudioDef *audio, case VIR_DOMAIN_AUDIO_TYPE_PULSEAUDIO: case VIR_DOMAIN_AUDIO_TYPE_SDL: case VIR_DOMAIN_AUDIO_TYPE_FILE: + case VIR_DOMAIN_AUDIO_TYPE_PIPEWIRE: break; case VIR_DOMAIN_AUDIO_TYPE_SPICE: diff --git a/tests/qemuxml2argvdata/audio-pipewire-best.xml b/tests/qemuxml2argvdata/audio-pipewire-best.xml new file mode 100644 index 0000000000..e71688d25b --- /dev/null +++ b/tests/qemuxml2argvdata/audio-pipewire-best.xml @@ -0,0 +1,43 @@ +<domain type='qemu'> + <name>QEMUGuest1</name> + <uuid>c7a5fdbd-edaf-9455-926a-d65c16db1809</uuid> + <memory unit='KiB'>219100</memory> + <currentMemory unit='KiB'>219100</currentMemory> + <vcpu placement='static'>1</vcpu> + <os> + <type arch='x86_64' machine='pc'>hvm</type> + <boot dev='cdrom'/> + </os> + <clock offset='utc'/> + <on_poweroff>destroy</on_poweroff> + <on_reboot>restart</on_reboot> + <on_crash>destroy</on_crash> + <devices> + <emulator>/usr/bin/qemu-system-x86_64</emulator> + <disk type='block' device='cdrom'> + <driver name='qemu' type='raw'/> + <source dev='/dev/cdrom'/> + <target dev='hdc' bus='ide'/> + <readonly/> + <address type='drive' controller='0' bus='1' target='0' unit='0'/> + </disk> + <controller type='usb' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/> + </controller> + <controller type='ide' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x1'/> + </controller> + <controller type='pci' index='0' model='pci-root'/> + <input type='mouse' bus='ps2'/> + <input type='keyboard' bus='ps2'/> + <audio id='1' type='pipewire' timerPeriod='50'> + <input mixingEngine='yes' fixedSettings='yes' voices='1' bufferLength='200' name='fish'> + <settings frequency='44100' channels='2' format='s16'/> + </input> + <output mixingEngine='yes' fixedSettings='yes' voices='2' bufferLength='200' name='fish'> + <settings frequency='22050' channels='4' format='f32'/> + </output> + </audio> + <memballoon model='none'/> + </devices> +</domain> diff --git a/tests/qemuxml2argvdata/audio-pipewire-full.xml b/tests/qemuxml2argvdata/audio-pipewire-full.xml new file mode 100644 index 0000000000..5811eef6d4 --- /dev/null +++ b/tests/qemuxml2argvdata/audio-pipewire-full.xml @@ -0,0 +1,43 @@ +<domain type='qemu'> + <name>QEMUGuest1</name> + <uuid>c7a5fdbd-edaf-9455-926a-d65c16db1809</uuid> + <memory unit='KiB'>219100</memory> + <currentMemory unit='KiB'>219100</currentMemory> + <vcpu placement='static'>1</vcpu> + <os> + <type arch='x86_64' machine='pc'>hvm</type> + <boot dev='cdrom'/> + </os> + <clock offset='utc'/> + <on_poweroff>destroy</on_poweroff> + <on_reboot>restart</on_reboot> + <on_crash>destroy</on_crash> + <devices> + <emulator>/usr/bin/qemu-system-x86_64</emulator> + <disk type='block' device='cdrom'> + <driver name='qemu' type='raw'/> + <source dev='/dev/cdrom'/> + <target dev='hdc' bus='ide'/> + <readonly/> + <address type='drive' controller='0' bus='1' target='0' unit='0'/> + </disk> + <controller type='usb' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/> + </controller> + <controller type='ide' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x1'/> + </controller> + <controller type='pci' index='0' model='pci-root'/> + <input type='mouse' bus='ps2'/> + <input type='keyboard' bus='ps2'/> + <audio id='1' type='pipewire'> + <input mixingEngine='yes' fixedSettings='yes' voices='1' bufferLength='100' name='fish' streamName='food' latency='100'> + <settings frequency='44100' channels='2' format='s16'/> + </input> + <output mixingEngine='yes' fixedSettings='yes' voices='2' bufferLength='200' name='fish' streamName='food' latency='200'> + <settings frequency='22050' channels='4' format='f32'/> + </output> + </audio> + <memballoon model='none'/> + </devices> +</domain> diff --git a/tests/qemuxml2argvdata/audio-pipewire-minimal.xml b/tests/qemuxml2argvdata/audio-pipewire-minimal.xml new file mode 100644 index 0000000000..2085225722 --- /dev/null +++ b/tests/qemuxml2argvdata/audio-pipewire-minimal.xml @@ -0,0 +1,36 @@ +<domain type='qemu'> + <name>QEMUGuest1</name> + <uuid>c7a5fdbd-edaf-9455-926a-d65c16db1809</uuid> + <memory unit='KiB'>219100</memory> + <currentMemory unit='KiB'>219100</currentMemory> + <vcpu placement='static'>1</vcpu> + <os> + <type arch='x86_64' machine='pc'>hvm</type> + <boot dev='cdrom'/> + </os> + <clock offset='utc'/> + <on_poweroff>destroy</on_poweroff> + <on_reboot>restart</on_reboot> + <on_crash>destroy</on_crash> + <devices> + <emulator>/usr/bin/qemu-system-x86_64</emulator> + <disk type='block' device='cdrom'> + <driver name='qemu' type='raw'/> + <source dev='/dev/cdrom'/> + <target dev='hdc' bus='ide'/> + <readonly/> + <address type='drive' controller='0' bus='1' target='0' unit='0'/> + </disk> + <controller type='usb' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/> + </controller> + <controller type='ide' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x1'/> + </controller> + <controller type='pci' index='0' model='pci-root'/> + <input type='mouse' bus='ps2'/> + <input type='keyboard' bus='ps2'/> + <audio id='1' type='pipewire'/> + <memballoon model='none'/> + </devices> +</domain> diff --git a/tests/qemuxml2xmloutdata/audio-pipewire-best.xml b/tests/qemuxml2xmloutdata/audio-pipewire-best.xml new file mode 100644 index 0000000000..e71688d25b --- /dev/null +++ b/tests/qemuxml2xmloutdata/audio-pipewire-best.xml @@ -0,0 +1,43 @@ +<domain type='qemu'> + <name>QEMUGuest1</name> + <uuid>c7a5fdbd-edaf-9455-926a-d65c16db1809</uuid> + <memory unit='KiB'>219100</memory> + <currentMemory unit='KiB'>219100</currentMemory> + <vcpu placement='static'>1</vcpu> + <os> + <type arch='x86_64' machine='pc'>hvm</type> + <boot dev='cdrom'/> + </os> + <clock offset='utc'/> + <on_poweroff>destroy</on_poweroff> + <on_reboot>restart</on_reboot> + <on_crash>destroy</on_crash> + <devices> + <emulator>/usr/bin/qemu-system-x86_64</emulator> + <disk type='block' device='cdrom'> + <driver name='qemu' type='raw'/> + <source dev='/dev/cdrom'/> + <target dev='hdc' bus='ide'/> + <readonly/> + <address type='drive' controller='0' bus='1' target='0' unit='0'/> + </disk> + <controller type='usb' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/> + </controller> + <controller type='ide' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x1'/> + </controller> + <controller type='pci' index='0' model='pci-root'/> + <input type='mouse' bus='ps2'/> + <input type='keyboard' bus='ps2'/> + <audio id='1' type='pipewire' timerPeriod='50'> + <input mixingEngine='yes' fixedSettings='yes' voices='1' bufferLength='200' name='fish'> + <settings frequency='44100' channels='2' format='s16'/> + </input> + <output mixingEngine='yes' fixedSettings='yes' voices='2' bufferLength='200' name='fish'> + <settings frequency='22050' channels='4' format='f32'/> + </output> + </audio> + <memballoon model='none'/> + </devices> +</domain> diff --git a/tests/qemuxml2xmloutdata/audio-pipewire-full.xml b/tests/qemuxml2xmloutdata/audio-pipewire-full.xml new file mode 100644 index 0000000000..5811eef6d4 --- /dev/null +++ b/tests/qemuxml2xmloutdata/audio-pipewire-full.xml @@ -0,0 +1,43 @@ +<domain type='qemu'> + <name>QEMUGuest1</name> + <uuid>c7a5fdbd-edaf-9455-926a-d65c16db1809</uuid> + <memory unit='KiB'>219100</memory> + <currentMemory unit='KiB'>219100</currentMemory> + <vcpu placement='static'>1</vcpu> + <os> + <type arch='x86_64' machine='pc'>hvm</type> + <boot dev='cdrom'/> + </os> + <clock offset='utc'/> + <on_poweroff>destroy</on_poweroff> + <on_reboot>restart</on_reboot> + <on_crash>destroy</on_crash> + <devices> + <emulator>/usr/bin/qemu-system-x86_64</emulator> + <disk type='block' device='cdrom'> + <driver name='qemu' type='raw'/> + <source dev='/dev/cdrom'/> + <target dev='hdc' bus='ide'/> + <readonly/> + <address type='drive' controller='0' bus='1' target='0' unit='0'/> + </disk> + <controller type='usb' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/> + </controller> + <controller type='ide' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x1'/> + </controller> + <controller type='pci' index='0' model='pci-root'/> + <input type='mouse' bus='ps2'/> + <input type='keyboard' bus='ps2'/> + <audio id='1' type='pipewire'> + <input mixingEngine='yes' fixedSettings='yes' voices='1' bufferLength='100' name='fish' streamName='food' latency='100'> + <settings frequency='44100' channels='2' format='s16'/> + </input> + <output mixingEngine='yes' fixedSettings='yes' voices='2' bufferLength='200' name='fish' streamName='food' latency='200'> + <settings frequency='22050' channels='4' format='f32'/> + </output> + </audio> + <memballoon model='none'/> + </devices> +</domain> diff --git a/tests/qemuxml2xmloutdata/audio-pipewire-minimal.xml b/tests/qemuxml2xmloutdata/audio-pipewire-minimal.xml new file mode 100644 index 0000000000..2085225722 --- /dev/null +++ b/tests/qemuxml2xmloutdata/audio-pipewire-minimal.xml @@ -0,0 +1,36 @@ +<domain type='qemu'> + <name>QEMUGuest1</name> + <uuid>c7a5fdbd-edaf-9455-926a-d65c16db1809</uuid> + <memory unit='KiB'>219100</memory> + <currentMemory unit='KiB'>219100</currentMemory> + <vcpu placement='static'>1</vcpu> + <os> + <type arch='x86_64' machine='pc'>hvm</type> + <boot dev='cdrom'/> + </os> + <clock offset='utc'/> + <on_poweroff>destroy</on_poweroff> + <on_reboot>restart</on_reboot> + <on_crash>destroy</on_crash> + <devices> + <emulator>/usr/bin/qemu-system-x86_64</emulator> + <disk type='block' device='cdrom'> + <driver name='qemu' type='raw'/> + <source dev='/dev/cdrom'/> + <target dev='hdc' bus='ide'/> + <readonly/> + <address type='drive' controller='0' bus='1' target='0' unit='0'/> + </disk> + <controller type='usb' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/> + </controller> + <controller type='ide' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x1'/> + </controller> + <controller type='pci' index='0' model='pci-root'/> + <input type='mouse' bus='ps2'/> + <input type='keyboard' bus='ps2'/> + <audio id='1' type='pipewire'/> + <memballoon model='none'/> + </devices> +</domain> diff --git a/tests/qemuxml2xmltest.c b/tests/qemuxml2xmltest.c index 93202e8e18..df019023d0 100644 --- a/tests/qemuxml2xmltest.c +++ b/tests/qemuxml2xmltest.c @@ -1175,6 +1175,7 @@ mymain(void) DO_TEST_NOCAPS("audio-coreaudio-minimal"); DO_TEST_NOCAPS("audio-oss-minimal"); DO_TEST_NOCAPS("audio-pulseaudio-minimal"); + DO_TEST_NOCAPS("audio-pipewire-minimal"); DO_TEST_NOCAPS("audio-sdl-minimal"); DO_TEST("audio-spice-minimal", QEMU_CAPS_SPICE, @@ -1187,6 +1188,7 @@ mymain(void) DO_TEST_NOCAPS("audio-coreaudio-best"); DO_TEST_NOCAPS("audio-oss-best"); DO_TEST_NOCAPS("audio-pulseaudio-best"); + DO_TEST_NOCAPS("audio-pipewire-best"); DO_TEST_NOCAPS("audio-sdl-best"); DO_TEST("audio-spice-best", QEMU_CAPS_SPICE, @@ -1200,6 +1202,7 @@ mymain(void) DO_TEST_NOCAPS("audio-jack-full"); DO_TEST_NOCAPS("audio-oss-full"); DO_TEST_NOCAPS("audio-pulseaudio-full"); + DO_TEST_NOCAPS("audio-pipewire-full"); DO_TEST_NOCAPS("audio-sdl-full"); DO_TEST("audio-spice-full", QEMU_CAPS_SPICE, -- 2.39.3

This is mostly straightforward, except for a teensy-weensy detail: usually, there's no system wide daemon running, no system wide available socket that anybody could connect to. PipeWire uses a per user daemon approach instead. But this in turn means, that the socket location floats between various locations and is derived from various environment variables (just like the actual socket name) and thus we must pass the variables to QEMU. Signed-off-by: Michal Privoznik <mprivozn@redhat.com> --- src/qemu/qemu_command.c | 61 +++++++++++++++++++ .../audio-many-backends.x86_64-latest.args | 2 + .../qemuxml2argvdata/audio-many-backends.xml | 1 + .../audio-pipewire-best.x86_64-latest.args | 36 +++++++++++ .../audio-pipewire-full.x86_64-latest.args | 36 +++++++++++ .../audio-pipewire-minimal.x86_64-latest.args | 36 +++++++++++ tests/qemuxml2argvtest.c | 12 ++++ 7 files changed, 184 insertions(+) create mode 100644 tests/qemuxml2argvdata/audio-pipewire-best.x86_64-latest.args create mode 100644 tests/qemuxml2argvdata/audio-pipewire-full.x86_64-latest.args create mode 100644 tests/qemuxml2argvdata/audio-pipewire-minimal.x86_64-latest.args diff --git a/src/qemu/qemu_command.c b/src/qemu/qemu_command.c index 3a08cac870..b31f4db6b1 100644 --- a/src/qemu/qemu_command.c +++ b/src/qemu/qemu_command.c @@ -7783,6 +7783,60 @@ qemuBuildAudioSDLProps(virDomainAudioIOSDL *def, } +static int +qemuBuildAudioPipewireAudioProps(virDomainAudioIOPipewireAudio *def, + virJSONValue **props) +{ + return virJSONValueObjectAdd(props, + "S:name", def->name, + "S:stream-name", def->streamName, + "p:latency", def->latency, + NULL); +} + + +static void +qemuBuildAudioPipewireAudioEnv(virCommand *cmd) +{ + const char *envVars[] = { "PIPEWIRE_RUNTIME_DIR", "XDG_RUNTIME_DIR", + "USERPROFILE" }; + size_t i; + + /* PipeWire needs access to its daemon socket. The socket name is + * configurable (core.name in pipewire.conf, or PIPEWIRE_CORE and + * PIPEWIRE_REMOTE env vars). If the socket name is not an absolute + * path, then the socket is looked for in the following directories + * (in order): + * + * - PIPEWIRE_RUNTIME_DIR + * - XDG_RUNTIME_DIR + * - USERPROFILE + * + * This order is defined in get_runtime_dir() from + * src/modules/module-protocol-native/local-socket.c from PipeWire's + * codebase. + * + * Now, PIPEWIRE_CORE and/or PIPEWIRE_REMOTE should be passed + * whenever present in the environment. But for the other three + * (socket location dirs), we can add just the first existing one + * (basically mimic get_runtime_dir() logic). + */ + + virCommandAddEnvPass(cmd, "PIPEWIRE_CORE"); + virCommandAddEnvPass(cmd, "PIPEWIRE_REMOTE"); + + for (i = 0; i < G_N_ELEMENTS(envVars); i++) { + const char *value = getenv(envVars[i]); + + if (!value) + continue; + + virCommandAddEnvPair(cmd, envVars[i], value); + break; + } +} + + static int qemuBuildAudioCommandLineArg(virCommand *cmd, virDomainAudioDef *def) @@ -7889,6 +7943,13 @@ qemuBuildAudioCommandLineArg(virCommand *cmd, break; case VIR_DOMAIN_AUDIO_TYPE_PIPEWIRE: + if (qemuBuildAudioPipewireAudioProps(&def->backend.pipewire.input, &in) < 0 || + qemuBuildAudioPipewireAudioProps(&def->backend.pipewire.output, &out) < 0) + return -1; + + qemuBuildAudioPipewireAudioEnv(cmd); + break; + case VIR_DOMAIN_AUDIO_TYPE_LAST: default: virReportEnumRangeError(virDomainAudioType, def->type); diff --git a/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args b/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args index 9caf591daf..13dd55054e 100644 --- a/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args +++ b/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args @@ -6,6 +6,7 @@ LOGNAME=test \ XDG_DATA_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.local/share \ XDG_CACHE_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.cache \ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \ +XDG_RUNTIME_DIR=/bad-test-used-env-xdg-runtime-dir \ /usr/bin/qemu-system-x86_64 \ -name guest=QEMUGuest1,debug-threads=on \ -S \ @@ -32,6 +33,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \ -audiodev '{"id":"audio1","driver":"none"}' \ -audiodev '{"id":"audio2","driver":"alsa"}' \ -audiodev '{"id":"audio3","driver":"pa"}' \ +-audiodev '{"id":"audio4","driver":"pipewire"}' \ -vnc 127.0.0.1:0,audiodev=audio2 \ -device '{"driver":"cirrus-vga","id":"video0","bus":"pci.0","addr":"0x2"}' \ -device '{"driver":"AC97","id":"sound0","audiodev":"audio1","bus":"pci.0","addr":"0x3"}' \ diff --git a/tests/qemuxml2argvdata/audio-many-backends.xml b/tests/qemuxml2argvdata/audio-many-backends.xml index c681784526..1659723f91 100644 --- a/tests/qemuxml2argvdata/audio-many-backends.xml +++ b/tests/qemuxml2argvdata/audio-many-backends.xml @@ -51,6 +51,7 @@ <audio id='1' type='none'/> <audio id='2' type='alsa'/> <audio id='3' type='pulseaudio'/> + <audio id='4' type='pipewire'/> <video> <model type='cirrus' vram='16384' heads='1' primary='yes'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/> diff --git a/tests/qemuxml2argvdata/audio-pipewire-best.x86_64-latest.args b/tests/qemuxml2argvdata/audio-pipewire-best.x86_64-latest.args new file mode 100644 index 0000000000..2e8635c1b0 --- /dev/null +++ b/tests/qemuxml2argvdata/audio-pipewire-best.x86_64-latest.args @@ -0,0 +1,36 @@ +LC_ALL=C \ +PATH=/bin \ +HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1 \ +USER=test \ +LOGNAME=test \ +XDG_DATA_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.local/share \ +XDG_CACHE_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.cache \ +XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \ +PIPEWIRE_RUNTIME_DIR=/run/user/1000 \ +/usr/bin/qemu-system-x86_64 \ +-name guest=QEMUGuest1,debug-threads=on \ +-S \ +-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \ +-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \ +-accel tcg \ +-cpu qemu64 \ +-m 214 \ +-object '{"qom-type":"memory-backend-ram","id":"pc.ram","size":224395264}' \ +-overcommit mem-lock=off \ +-smp 1,sockets=1,cores=1,threads=1 \ +-uuid c7a5fdbd-edaf-9455-926a-d65c16db1809 \ +-display none \ +-no-user-config \ +-nodefaults \ +-chardev socket,id=charmonitor,fd=1729,server=on,wait=off \ +-mon chardev=charmonitor,id=monitor,mode=control \ +-rtc base=utc \ +-no-shutdown \ +-boot strict=on \ +-device '{"driver":"piix3-usb-uhci","id":"usb","bus":"pci.0","addr":"0x1.0x2"}' \ +-blockdev '{"driver":"host_cdrom","filename":"/dev/cdrom","node-name":"libvirt-1-storage","auto-read-only":true,"discard":"unmap"}' \ +-blockdev '{"node-name":"libvirt-1-format","read-only":true,"driver":"raw","file":"libvirt-1-storage"}' \ +-device '{"driver":"ide-cd","bus":"ide.1","unit":0,"drive":"libvirt-1-format","id":"ide0-1-0","bootindex":1}' \ +-audiodev '{"id":"audio1","driver":"pipewire","timer-period":50,"in":{"mixing-engine":true,"fixed-settings":true,"voices":1,"buffer-length":200,"frequency":44100,"channels":2,"format":"s16","name":"fish"},"out":{"mixing-engine":true,"fixed-settings":true,"voices":2,"buffer-length":200,"frequency":22050,"channels":4,"format":"f32","name":"fish"}}' \ +-sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny \ +-msg timestamp=on diff --git a/tests/qemuxml2argvdata/audio-pipewire-full.x86_64-latest.args b/tests/qemuxml2argvdata/audio-pipewire-full.x86_64-latest.args new file mode 100644 index 0000000000..caed694823 --- /dev/null +++ b/tests/qemuxml2argvdata/audio-pipewire-full.x86_64-latest.args @@ -0,0 +1,36 @@ +LC_ALL=C \ +PATH=/bin \ +HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1 \ +USER=test \ +LOGNAME=test \ +XDG_DATA_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.local/share \ +XDG_CACHE_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.cache \ +XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \ +PIPEWIRE_RUNTIME_DIR=/run/user/1000 \ +/usr/bin/qemu-system-x86_64 \ +-name guest=QEMUGuest1,debug-threads=on \ +-S \ +-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \ +-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \ +-accel tcg \ +-cpu qemu64 \ +-m 214 \ +-object '{"qom-type":"memory-backend-ram","id":"pc.ram","size":224395264}' \ +-overcommit mem-lock=off \ +-smp 1,sockets=1,cores=1,threads=1 \ +-uuid c7a5fdbd-edaf-9455-926a-d65c16db1809 \ +-display none \ +-no-user-config \ +-nodefaults \ +-chardev socket,id=charmonitor,fd=1729,server=on,wait=off \ +-mon chardev=charmonitor,id=monitor,mode=control \ +-rtc base=utc \ +-no-shutdown \ +-boot strict=on \ +-device '{"driver":"piix3-usb-uhci","id":"usb","bus":"pci.0","addr":"0x1.0x2"}' \ +-blockdev '{"driver":"host_cdrom","filename":"/dev/cdrom","node-name":"libvirt-1-storage","auto-read-only":true,"discard":"unmap"}' \ +-blockdev '{"node-name":"libvirt-1-format","read-only":true,"driver":"raw","file":"libvirt-1-storage"}' \ +-device '{"driver":"ide-cd","bus":"ide.1","unit":0,"drive":"libvirt-1-format","id":"ide0-1-0","bootindex":1}' \ +-audiodev '{"id":"audio1","driver":"pipewire","in":{"mixing-engine":true,"fixed-settings":true,"voices":1,"buffer-length":100,"frequency":44100,"channels":2,"format":"s16","name":"fish","stream-name":"food","latency":100},"out":{"mixing-engine":true,"fixed-settings":true,"voices":2,"buffer-length":200,"frequency":22050,"channels":4,"format":"f32","name":"fish","stream-name":"food","latency":200}}' \ +-sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny \ +-msg timestamp=on diff --git a/tests/qemuxml2argvdata/audio-pipewire-minimal.x86_64-latest.args b/tests/qemuxml2argvdata/audio-pipewire-minimal.x86_64-latest.args new file mode 100644 index 0000000000..2e76997d58 --- /dev/null +++ b/tests/qemuxml2argvdata/audio-pipewire-minimal.x86_64-latest.args @@ -0,0 +1,36 @@ +LC_ALL=C \ +PATH=/bin \ +HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1 \ +USER=test \ +LOGNAME=test \ +XDG_DATA_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.local/share \ +XDG_CACHE_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.cache \ +XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \ +PIPEWIRE_RUNTIME_DIR=/run/user/1000 \ +/usr/bin/qemu-system-x86_64 \ +-name guest=QEMUGuest1,debug-threads=on \ +-S \ +-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \ +-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \ +-accel tcg \ +-cpu qemu64 \ +-m 214 \ +-object '{"qom-type":"memory-backend-ram","id":"pc.ram","size":224395264}' \ +-overcommit mem-lock=off \ +-smp 1,sockets=1,cores=1,threads=1 \ +-uuid c7a5fdbd-edaf-9455-926a-d65c16db1809 \ +-display none \ +-no-user-config \ +-nodefaults \ +-chardev socket,id=charmonitor,fd=1729,server=on,wait=off \ +-mon chardev=charmonitor,id=monitor,mode=control \ +-rtc base=utc \ +-no-shutdown \ +-boot strict=on \ +-device '{"driver":"piix3-usb-uhci","id":"usb","bus":"pci.0","addr":"0x1.0x2"}' \ +-blockdev '{"driver":"host_cdrom","filename":"/dev/cdrom","node-name":"libvirt-1-storage","auto-read-only":true,"discard":"unmap"}' \ +-blockdev '{"node-name":"libvirt-1-format","read-only":true,"driver":"raw","file":"libvirt-1-storage"}' \ +-device '{"driver":"ide-cd","bus":"ide.1","unit":0,"drive":"libvirt-1-format","id":"ide0-1-0","bootindex":1}' \ +-audiodev '{"id":"audio1","driver":"pipewire"}' \ +-sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny \ +-msg timestamp=on diff --git a/tests/qemuxml2argvtest.c b/tests/qemuxml2argvtest.c index 3100078b54..f29597a19f 100644 --- a/tests/qemuxml2argvtest.c +++ b/tests/qemuxml2argvtest.c @@ -926,6 +926,9 @@ mymain(void) g_unsetenv("DYLD_FORCE_FLAT_NAMESPACE"); g_unsetenv("QEMU_AUDIO_DRV"); g_unsetenv("SDL_AUDIODRIVER"); + g_unsetenv("PIPEWIRE_CORE"); + g_unsetenv("PIPEWIRE_REMOTE"); + g_unsetenv("PIPEWIRE_RUNTIME_DIR"); DO_TEST_NOCAPS("minimal"); DO_TEST_PARSE_ERROR_NOCAPS("minimal-no-memory"); @@ -967,6 +970,9 @@ mymain(void) DO_TEST_CAPS_LATEST("audio-jack-minimal"); DO_TEST_CAPS_LATEST("audio-oss-minimal"); DO_TEST_CAPS_LATEST("audio-pulseaudio-minimal"); + g_setenv("PIPEWIRE_RUNTIME_DIR", "/run/user/1000", TRUE); + DO_TEST_CAPS_LATEST("audio-pipewire-minimal"); + g_unsetenv("PIPEWIRE_RUNTIME_DIR"); DO_TEST_CAPS_LATEST("audio-sdl-minimal"); DO_TEST_CAPS_LATEST("audio-spice-minimal"); DO_TEST_CAPS_LATEST("audio-file-minimal"); @@ -976,6 +982,9 @@ mymain(void) DO_TEST_CAPS_LATEST("audio-coreaudio-best"); DO_TEST_CAPS_LATEST("audio-oss-best"); DO_TEST_CAPS_LATEST("audio-pulseaudio-best"); + g_setenv("PIPEWIRE_RUNTIME_DIR", "/run/user/1000", TRUE); + DO_TEST_CAPS_LATEST("audio-pipewire-best"); + g_unsetenv("PIPEWIRE_RUNTIME_DIR"); DO_TEST_CAPS_LATEST("audio-sdl-best"); DO_TEST_CAPS_LATEST("audio-spice-best"); DO_TEST_CAPS_LATEST("audio-file-best"); @@ -986,6 +995,9 @@ mymain(void) DO_TEST_CAPS_LATEST("audio-jack-full"); DO_TEST_CAPS_LATEST("audio-oss-full"); DO_TEST_CAPS_LATEST("audio-pulseaudio-full"); + g_setenv("PIPEWIRE_RUNTIME_DIR", "/run/user/1000", TRUE); + DO_TEST_CAPS_LATEST("audio-pipewire-full"); + g_unsetenv("PIPEWIRE_RUNTIME_DIR"); DO_TEST_CAPS_LATEST("audio-sdl-full"); DO_TEST_CAPS_LATEST("audio-spice-full"); DO_TEST_CAPS_LATEST("audio-file-full"); -- 2.39.3

On Thu, May 11, 2023 at 02:14:51PM +0200, Michal Privoznik wrote:
This is mostly straightforward, except for a teensy-weensy detail: usually, there's no system wide daemon running, no system wide available socket that anybody could connect to. PipeWire uses a per user daemon approach instead. But this in turn means,
I spent some time thinking about this even after I said "just give it runtime dir" and now I'm wondering, is this not similar to the dbus daemon? When we launch it with a constructed config file for graphics type='dbus' we could also launch pipewire deamon with our config file and let qemu connect to that daemon. I'm not sure what the audio permissions would look like, but it seems like a less messy approach.
that the socket location floats between various locations and is derived from various environment variables (just like the actual socket name) and thus we must pass the variables to QEMU.
Signed-off-by: Michal Privoznik <mprivozn@redhat.com> --- src/qemu/qemu_command.c | 61 +++++++++++++++++++ .../audio-many-backends.x86_64-latest.args | 2 + .../qemuxml2argvdata/audio-many-backends.xml | 1 + .../audio-pipewire-best.x86_64-latest.args | 36 +++++++++++ .../audio-pipewire-full.x86_64-latest.args | 36 +++++++++++ .../audio-pipewire-minimal.x86_64-latest.args | 36 +++++++++++ tests/qemuxml2argvtest.c | 12 ++++ 7 files changed, 184 insertions(+) create mode 100644 tests/qemuxml2argvdata/audio-pipewire-best.x86_64-latest.args create mode 100644 tests/qemuxml2argvdata/audio-pipewire-full.x86_64-latest.args create mode 100644 tests/qemuxml2argvdata/audio-pipewire-minimal.x86_64-latest.args
diff --git a/src/qemu/qemu_command.c b/src/qemu/qemu_command.c index 3a08cac870..b31f4db6b1 100644 --- a/src/qemu/qemu_command.c +++ b/src/qemu/qemu_command.c @@ -7783,6 +7783,60 @@ qemuBuildAudioSDLProps(virDomainAudioIOSDL *def, }
+static int +qemuBuildAudioPipewireAudioProps(virDomainAudioIOPipewireAudio *def, + virJSONValue **props) +{ + return virJSONValueObjectAdd(props, + "S:name", def->name, + "S:stream-name", def->streamName, + "p:latency", def->latency, + NULL); +} + + +static void +qemuBuildAudioPipewireAudioEnv(virCommand *cmd) +{ + const char *envVars[] = { "PIPEWIRE_RUNTIME_DIR", "XDG_RUNTIME_DIR", + "USERPROFILE" }; + size_t i; + + /* PipeWire needs access to its daemon socket. The socket name is + * configurable (core.name in pipewire.conf, or PIPEWIRE_CORE and + * PIPEWIRE_REMOTE env vars). If the socket name is not an absolute + * path, then the socket is looked for in the following directories + * (in order): + * + * - PIPEWIRE_RUNTIME_DIR + * - XDG_RUNTIME_DIR + * - USERPROFILE + * + * This order is defined in get_runtime_dir() from + * src/modules/module-protocol-native/local-socket.c from PipeWire's + * codebase. + * + * Now, PIPEWIRE_CORE and/or PIPEWIRE_REMOTE should be passed + * whenever present in the environment. But for the other three + * (socket location dirs), we can add just the first existing one + * (basically mimic get_runtime_dir() logic). + */ + + virCommandAddEnvPass(cmd, "PIPEWIRE_CORE"); + virCommandAddEnvPass(cmd, "PIPEWIRE_REMOTE"); + + for (i = 0; i < G_N_ELEMENTS(envVars); i++) { + const char *value = getenv(envVars[i]); + + if (!value) + continue; + + virCommandAddEnvPair(cmd, envVars[i], value); + break; + } +} + + static int qemuBuildAudioCommandLineArg(virCommand *cmd, virDomainAudioDef *def) @@ -7889,6 +7943,13 @@ qemuBuildAudioCommandLineArg(virCommand *cmd, break;
case VIR_DOMAIN_AUDIO_TYPE_PIPEWIRE: + if (qemuBuildAudioPipewireAudioProps(&def->backend.pipewire.input, &in) < 0 || + qemuBuildAudioPipewireAudioProps(&def->backend.pipewire.output, &out) < 0) + return -1; + + qemuBuildAudioPipewireAudioEnv(cmd); + break; + case VIR_DOMAIN_AUDIO_TYPE_LAST: default: virReportEnumRangeError(virDomainAudioType, def->type); diff --git a/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args b/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args index 9caf591daf..13dd55054e 100644 --- a/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args +++ b/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args @@ -6,6 +6,7 @@ LOGNAME=test \ XDG_DATA_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.local/share \ XDG_CACHE_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.cache \ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \ +XDG_RUNTIME_DIR=/bad-test-used-env-xdg-runtime-dir \
This should fail the test suite IIRC.

On 5/16/23 11:52, Martin Kletzander wrote:
On Thu, May 11, 2023 at 02:14:51PM +0200, Michal Privoznik wrote:
This is mostly straightforward, except for a teensy-weensy detail: usually, there's no system wide daemon running, no system wide available socket that anybody could connect to. PipeWire uses a per user daemon approach instead. But this in turn means,
I spent some time thinking about this even after I said "just give it runtime dir" and now I'm wondering, is this not similar to the dbus daemon? When we launch it with a constructed config file for graphics type='dbus' we could also launch pipewire deamon with our config file and let qemu connect to that daemon. I'm not sure what the audio permissions would look like, but it seems like a less messy approach.
And how would then this per-domain pipewire daemon talk to the user session daemon that's actually doing all the mixing? I mean, how would sound from a domain get to speakers/headphones? For dbus it's fairly easy to use, because the socket (and config file) is located under configurable location (cfg->dbusStateDir) and with predictable name (domain short name) AND (more importantly) we have virDomainOpenGraphics() API which then handles virt-viewer connection requests.
that the socket location floats between various locations and is derived from various environment variables (just like the actual socket name) and thus we must pass the variables to QEMU.
Signed-off-by: Michal Privoznik <mprivozn@redhat.com> --- src/qemu/qemu_command.c | 61 +++++++++++++++++++ .../audio-many-backends.x86_64-latest.args | 2 + .../qemuxml2argvdata/audio-many-backends.xml | 1 + .../audio-pipewire-best.x86_64-latest.args | 36 +++++++++++ .../audio-pipewire-full.x86_64-latest.args | 36 +++++++++++ .../audio-pipewire-minimal.x86_64-latest.args | 36 +++++++++++ tests/qemuxml2argvtest.c | 12 ++++ 7 files changed, 184 insertions(+) create mode 100644 tests/qemuxml2argvdata/audio-pipewire-best.x86_64-latest.args create mode 100644 tests/qemuxml2argvdata/audio-pipewire-full.x86_64-latest.args create mode 100644 tests/qemuxml2argvdata/audio-pipewire-minimal.x86_64-latest.args
diff --git a/src/qemu/qemu_command.c b/src/qemu/qemu_command.c index 3a08cac870..b31f4db6b1 100644 --- a/src/qemu/qemu_command.c +++ b/src/qemu/qemu_command.c @@ -7783,6 +7783,60 @@ qemuBuildAudioSDLProps(virDomainAudioIOSDL *def, }
+static int +qemuBuildAudioPipewireAudioProps(virDomainAudioIOPipewireAudio *def, + virJSONValue **props) +{ + return virJSONValueObjectAdd(props, + "S:name", def->name, + "S:stream-name", def->streamName, + "p:latency", def->latency, + NULL); +} + + +static void +qemuBuildAudioPipewireAudioEnv(virCommand *cmd) +{ + const char *envVars[] = { "PIPEWIRE_RUNTIME_DIR", "XDG_RUNTIME_DIR", + "USERPROFILE" }; + size_t i; + + /* PipeWire needs access to its daemon socket. The socket name is + * configurable (core.name in pipewire.conf, or PIPEWIRE_CORE and + * PIPEWIRE_REMOTE env vars). If the socket name is not an absolute + * path, then the socket is looked for in the following directories + * (in order): + * + * - PIPEWIRE_RUNTIME_DIR + * - XDG_RUNTIME_DIR + * - USERPROFILE + * + * This order is defined in get_runtime_dir() from + * src/modules/module-protocol-native/local-socket.c from PipeWire's + * codebase. + * + * Now, PIPEWIRE_CORE and/or PIPEWIRE_REMOTE should be passed + * whenever present in the environment. But for the other three + * (socket location dirs), we can add just the first existing one + * (basically mimic get_runtime_dir() logic). + */ + + virCommandAddEnvPass(cmd, "PIPEWIRE_CORE"); + virCommandAddEnvPass(cmd, "PIPEWIRE_REMOTE"); + + for (i = 0; i < G_N_ELEMENTS(envVars); i++) { + const char *value = getenv(envVars[i]); + + if (!value) + continue; + + virCommandAddEnvPair(cmd, envVars[i], value); + break; + } +} + + static int qemuBuildAudioCommandLineArg(virCommand *cmd, virDomainAudioDef *def) @@ -7889,6 +7943,13 @@ qemuBuildAudioCommandLineArg(virCommand *cmd, break;
case VIR_DOMAIN_AUDIO_TYPE_PIPEWIRE: + if (qemuBuildAudioPipewireAudioProps(&def->backend.pipewire.input, &in) < 0 || + qemuBuildAudioPipewireAudioProps(&def->backend.pipewire.output, &out) < 0) + return -1; + + qemuBuildAudioPipewireAudioEnv(cmd); + break; + case VIR_DOMAIN_AUDIO_TYPE_LAST: default: virReportEnumRangeError(virDomainAudioType, def->type); diff --git a/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args b/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args index 9caf591daf..13dd55054e 100644 --- a/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args +++ b/tests/qemuxml2argvdata/audio-many-backends.x86_64-latest.args @@ -6,6 +6,7 @@ LOGNAME=test \ XDG_DATA_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.local/share \ XDG_CACHE_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.cache \ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \ +XDG_RUNTIME_DIR=/bad-test-used-env-xdg-runtime-dir \
This should fail the test suite IIRC.
I'm failing to see why. The poisoning was done so that things like: virDirCreate("${XGD_RUNTIME_DIR}/something/something"); fail. It's okay if it's in environment. But for everybody's peace of mind, I can squash this in: diff --git i/tests/qemuxml2argvtest.c w/tests/qemuxml2argvtest.c index f29597a19f..37c2ab0826 100644 --- i/tests/qemuxml2argvtest.c +++ w/tests/qemuxml2argvtest.c @@ -1002,7 +1002,9 @@ mymain(void) DO_TEST_CAPS_LATEST("audio-spice-full"); DO_TEST_CAPS_LATEST("audio-file-full"); + g_setenv("PIPEWIRE_RUNTIME_DIR", "/run/user/1000", TRUE); DO_TEST_CAPS_LATEST("audio-many-backends"); + g_unsetenv("PIPEWIRE_RUNTIME_DIR"); /* Validate auto-creation of <audio> for legacy compat */ g_setenv("QEMU_AUDIO_DRV", "sdl", TRUE); Michal

On Tue, May 16, 2023 at 12:49:02PM +0200, Michal Prívozník wrote:
On 5/16/23 11:52, Martin Kletzander wrote:
On Thu, May 11, 2023 at 02:14:51PM +0200, Michal Privoznik wrote:
This is mostly straightforward, except for a teensy-weensy detail: usually, there's no system wide daemon running, no system wide available socket that anybody could connect to. PipeWire uses a per user daemon approach instead. But this in turn means,
I spent some time thinking about this even after I said "just give it runtime dir" and now I'm wondering, is this not similar to the dbus daemon? When we launch it with a constructed config file for graphics type='dbus' we could also launch pipewire deamon with our config file and let qemu connect to that daemon. I'm not sure what the audio permissions would look like, but it seems like a less messy approach.
And how would then this per-domain pipewire daemon talk to the user session daemon that's actually doing all the mixing? I mean, how would sound from a domain get to speakers/headphones?
For dbus it's fairly easy to use, because the socket (and config file) is located under configurable location (cfg->dbusStateDir) and with predictable name (domain short name) AND (more importantly) we have virDomainOpenGraphics() API which then handles virt-viewer connection requests.
I'm not sure how pipewire works in this context, my idea was that it would not go through the user's daemon at all, that would only be done for qemu:///session. But maybe my understanding is incomplete (highly probable).

Signed-off-by: Michal Privoznik <mprivozn@redhat.com> --- NEWS.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index 446998e12e..ddec27cd57 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -17,6 +17,16 @@ v9.4.0 (unreleased) * **New features** + * Introduce pipewire audio backend + + The QEMU hypervisor driver now allows setting ``pipewire`` backend for + ``<audio/>`` device. Please note, that for domains under system URI it + might be necessary to set various environment variables (e.g. + ``PIPEWIRE_REMOTE`` and/or ``PIPEWIRE_RUNTIME_DIR``) to point QEMU process + to a PipeWire daemon running under a non-privileged user. See `knowledge base + <https://libvirt.org/kbase/qemu-passthrough-security.html#xml-document-additions>`_ + for more information on passing additional environment variables. + * **Improvements** * **Bug fixes** -- 2.39.3

On Thu, May 11, 2023 at 14:14:49 +0200, Michal Privoznik wrote:
You'd expect to see src/qemu/qemu_capabilities.c changed, wouldn't you. Well, there's no way to query for supported audio backends, see:
Note that you can already query whether qemu was _compiled_ with the support for an audio backend. You can't query whether (if modular build is done) the audio backend driver is actually installed. In your case it would be done by: { "query-audiodevs/ret-type/+pipewire", QEMU_CAPS_QUERY_AUDIODEVS_PIPEWIRE } and would require a new capability dump. I can do the latter if you want. Given that here it's only a explicit user configuration, I don't think it's entirely necessary to have capability checks.
participants (4)
-
Martin Kletzander
-
Michal Privoznik
-
Michal Prívozník
-
Peter Krempa