diff --git a/Makefile.in b/Makefile.in
index e7f44c098c4efdcf32b71a169a9303c967c00fef..67b199bbc822739c7662ffceb4d4ec031d045856 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -38,7 +38,7 @@ gdb:
 	$(MAKE) run DBG=gdb
 
 valgrind:
-	$(MAKE) run DBG="DISABLE_RTKIT=1 valgrind --trace-children=yes"
+	$(MAKE) run DBG="DISABLE_RTKIT=1 PIPEWIRE_DLCLOSE=false valgrind --trace-children=yes"
 
 test: all
 	ninja -C $(BUILD_ROOT) test
diff --git a/NEWS b/NEWS
index f2c768108514ee639799e07b6ae68111d1b2720b..971e31271b666e8fd0ed13a161adabb81e0fbce1 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,121 @@
+# PipeWire 0.3.63 (2022-12-15)
+
+This is a quick bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+  - Fix a critical bug that causes audio distortion in some cases when using
+    AVX2.
+  - Fix a crash in mpv caused by deinit of PipeWire.
+  - Resample the convolver IR to match the graph samplerate for better
+    results.
+  - Many more small bugfixes and improvements.
+
+
+## PipeWire
+  - Fix a segfault in the PipeWire deinit code triggered by mpv in some
+    cases. (#2881)
+  - Fix docs about SPA_PLUGIN_DIR.
+  - Always dlclose by default (even under valgrind). Add an option with
+    PIPEWIRE_DLCLOSE to select alternative behaviour.
+  - Improve PIPEWIRE_DEBUG category handling.
+
+## modules
+  - Resample the IR for the convolver when the IR samplerate and graph rate
+    don't match.
+
+## SPA
+  - Handle spurious reads from timerfd gracefully.
+  - Fix potential stack-use-after-scope when starting Audacity.
+  - Fix distorted audio when using AVX2. (#2885)
+  - Remove fallback to default channel map in channelmix.
+  - Improve sorting of MIDI events, use the same order as Ardour. (#1816)
+  - Enable LFE downmixing by default. (#2425)
+  - Make IEC958/AC3 and IEC958/DTS work better by enforcing a fixed minimal
+    buffering for the encoder to avoid stuttering. (#2650)
+
+## Pulse-Server
+  - Add a new pulse.cmd config section to execute pulse commands, currently
+    only for loading modules. This removes the dependency on pactl.
+  - Improve debug of messages.
+
+
+Older versions:
+
+# PipeWire 0.3.62 (2022-12-09)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+  - A regression in screensharing was fixed. It was caused by a race when
+    activating links and driver nodes.
+  - Video transform metadata was added so that cameras and screen sharing
+    can report the video orientation and transformations.
+  - Support for the PulseAudio module-gsettings was added to make paprefs
+    work.
+  - Support for bluetooth offloading was added. This allows for the bluetooth
+    reception, decoding and playback to happen completely in hardware.
+    This also requires some support in WirePlumber.
+  - Many bugfixes and improvements.
+
+
+## PipeWire
+  - More work on stopping nodes in a more controlled way.
+  - Fix a race in starting nodes and drivers. In some cases the driver
+    node would already be started while the link to the peer node was not
+    ready yet. This caused regressions in screen sharing. The driver is
+    now only started after all the followers and links completed.
+  - Fix a case where a slow capture stream would not recycle buffers
+    anymore and stall. (#2874)
+  - Fix a subtle bug in pw_loop_invoke that could cause callbacks to be
+    delayed and cause crashes in some cases.
+  - Fix a case where IPC was done from the data-thread and could cause
+    crashes.
+
+## Tools
+  - Silence some expected errors in the pw-top output.
+
+## modules
+  - The filter-chain has seen some optimizations in the copy plugin and
+    the convolver.
+  - The zeroconf plugin will now only unpublish services from the server
+    that was removed.
+  - Fix a potential crash when stopping pw-loopback.
+  - Some harmless errors were turned into info messages.
+  - Fix some cases where pw_stream methods were called from the data-thread
+    that could cause segfaults. (#2633)
+
+## SPA
+  - There is now a video transform metadata that indicates how a video
+    frame was transformed (rotated/flipped). libcamera and the GStreamer
+    elements now have support for this metadata.
+  - The SPA volume plugin is now disabled from the default build.
+  - Handle missing control info in libcamera.
+  - Handle errors from loop better, don't call the callbacks on errors.
+  - Somewhat improve performance in some audioconvert AVX2 code for format
+    conversion.
+  - Fix PortConfig and EnumPortConfig params in audioconvert and
+    audioadapter to reflect what is actually going on instead of using
+    hardcoded values.
+  - Pass ignore-dB property correctly in all cases.
+  - Probing is now done in 48KHz again. (#2857)
+
+## Pulse-server
+  - IPv4 addresses are now added first to the list and exposed first with
+    zeroconf discover.
+  - module-gsettings was added to make paprefs work.
+  - The pulse.idle.timeout option was disabled by default and only enabled
+    for selected apps (speech-dispatcher) because it caused some problems
+    for other apps. (#2880)
+
+## JACK
+  - Only process valid ports. Could fix some crashes. (#2863)
+
+## Bluetooth
+  - Support was added for offloading bluetooth handling. Some hardware can
+    receive, decode and play the bluetooth audio directly in hardware.
+
 # PipeWire 0.3.61 (2022-11-24)
 
 This is a bugfix release that is API and ABI compatible with previous
@@ -65,9 +183,6 @@ This is a bugfix release that is API and ABI compatible with previous
   - Add option to set node.passive on jack clients. Make some quirks
     for qsynth to make it suspend and fade out better.
 
-
-Older versions:
-
 # PipeWire 0.3.60 (2022-11-10)
 
 This is a bugfix release that is API and ABI compatible with previous
diff --git a/debian/changelog b/debian/changelog
index 758f8ddcda3ac31cababbedfe9bacf2f2666913c..e6d1fdaec3a56b83eb54c4b2a8cbf684355daa7e 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,36 @@
+pipewire (0.3.63-1~bpo11+1) bullseye-backports; urgency=medium
+
+  * Rebuild for bullseye-backports.
+  * Disable XFixes, minimum required version not available in Bullseye
+  * Disable libcamera, not available in Bullseye
+  * Disable liblc3, not available in Bullseye
+  * Do not mark pipewire-alsa in conflict with pulseaudio
+      This will break dependencies in Bullseye.
+
+ -- Dylan Aïssi <daissi@debian.org>  Tue, 03 Jan 2023 11:31:01 +0100
+
+pipewire (0.3.63-1) unstable; urgency=medium
+
+  * New upstream release
+      - pw-mon: recognize -N and -C as valid options (Closes: #1025900)
+  * Update symbols file
+  * Re-enable libcamera plugin for x32
+
+ -- Dylan Aïssi <daissi@debian.org>  Thu, 15 Dec 2022 12:07:57 +0100
+
+pipewire (0.3.62-1) unstable; urgency=medium
+
+  * New upstream release
+  * Don't build the legacy volume SPA plugin, as per upstream: this
+     volume plugin was an experiment that's not really used anywhere.
+  * Disable libcamera plugin for m68k and x32,
+     because the libcamera minimum version is not available.
+  * Install upstream NEWS file in /usr/share/doc/pipewire instead of
+     /usr/share/doc/libpipewire-0.3-0/ to make it easier to find.
+     (Closes: #1024815)
+
+ -- Dylan Aïssi <daissi@debian.org>  Fri, 09 Dec 2022 14:03:00 +0100
+
 pipewire (0.3.61-1~bpo11+1+apertis2) apertis; urgency=medium
 
   * apparmor: fix rule for /proc/*/task/*/comm
diff --git a/debian/control b/debian/control
index 8f2b45111bc1a906ee009cb3d30964d98ca6e540..6aa24bcb275fd2838298fe7d7bb5a60b83b8b3ae 100644
--- a/debian/control
+++ b/debian/control
@@ -11,7 +11,7 @@ Build-Depends: debhelper-compat (= 13),
                libasound2-dev,
                libavahi-client-dev,
                libbluetooth-dev,
-#               libcamera-dev (>= 0.0.1),
+#               libcamera-dev (>= 0.0.1) [!m68k],
                libdbus-1-dev,
 #               libfreeaptx-dev,
                libglib2.0-dev,
@@ -309,7 +309,7 @@ Description: PipeWire V4L2 plugin
 
 #Package: pipewire-libcamera
 #Section: video
-#Architecture: linux-any
+#Architecture: amd64 arm64 armel armhf i386 mips64el mipsel ppc64el s390x alpha hppa ia64 powerpc ppc64 riscv64 sh4 sparc64 x32
 #Multi-Arch: same
 #Depends: pipewire (= ${binary:Version}),
 #         ${misc:Depends},
diff --git a/debian/libpipewire-0.3-0.symbols b/debian/libpipewire-0.3-0.symbols
index 84324498643447c537e623198e0e4c397eefdc4a..15998a6de7b0ec0d3266f6ab32df7220b0ed69db 100644
--- a/debian/libpipewire-0.3-0.symbols
+++ b/debian/libpipewire-0.3-0.symbols
@@ -376,6 +376,7 @@ libpipewire-0.3.so.0 libpipewire-0.3-0 #MINVER#
  pw_resource_set_bound_id@Base 0.3.1
  pw_resource_unref@Base 0.3.52
  pw_set_domain@Base 0.3.26
+ pw_split_ip@Base 0.3.63
  pw_split_strv@Base 0.3.1
  pw_split_walk@Base 0.3.1
  pw_stream_add_listener@Base 0.3.1
diff --git a/debian/libspa-0.2-modules.install b/debian/libspa-0.2-modules.install
index 7accb5b3c0cfc6ef6ba4db449553f847ac6c3855..a4063f08a07cc2f42bf976eae6b966254cf4d515 100644
--- a/debian/libspa-0.2-modules.install
+++ b/debian/libspa-0.2-modules.install
@@ -10,4 +10,3 @@ usr/lib/*/spa-0.2/test
 usr/lib/*/spa-0.2/v4l2
 usr/lib/*/spa-0.2/videoconvert
 usr/lib/*/spa-0.2/videotestsrc
-usr/lib/*/spa-0.2/volume
diff --git a/debian/docs b/debian/pipewire.docs
similarity index 100%
rename from debian/docs
rename to debian/pipewire.docs
diff --git a/debian/rules b/debian/rules
index 643481a8b3e45e4f7e28d560c00ed5afbba04f0e..34b1b970653170e6b240f1f2e5c2d11ffdc83844 100755
--- a/debian/rules
+++ b/debian/rules
@@ -18,6 +18,12 @@ else
 BLUEZ5_CODEC_LDAC=enabled
 endif
 
+ifneq (,$(filter m68k,$(DEB_HOST_ARCH)))
+LIBCAMERA=disabled
+else
+LIBCAMERA=enabled
+endif
+
 # lilv and some of its dependencies are in universe
 ifeq (yes,$(shell dpkg-vendor --derives-from Ubuntu && echo yes))
 LV2=disabled
@@ -46,7 +52,6 @@ override_dh_auto_configure:
 		-Dsession-managers= \
 		-Dtest=enabled \
 		-Dvideotestsrc=enabled \
-		-Dvolume=enabled \
 		-Dvulkan=disabled \
 		-Dsdl2=disabled \
 		-Djack=disabled \
diff --git a/doc/spa-plugins.dox b/doc/spa-plugins.dox
index 4ab50000ccfddbc133596a3a81b4e222fc4b161d..af14d5eb19115fd155ff1a5af3ad01ee0ac6ef4f 100644
--- a/doc/spa-plugins.dox
+++ b/doc/spa-plugins.dox
@@ -19,7 +19,7 @@ To use a plugin, the following steps are required:
 In pseudo-code, loading a logger interface looks like this:
 
 \code{.py}
-handle = dlopen("$SPA_PLUGIN_PATH/support/libspa-support.so")
+handle = dlopen("$SPA_PLUGIN_DIR/support/libspa-support.so")
 factory_enumeration_func = dlsym(handle, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)
 spa_log *logger = NULL
 
@@ -49,8 +49,8 @@ are versioned and many versions can live on the same system.
 The `spa-inspect` tool provides a CLI interface to inspect SPA plugins:
 
 \verbatim
-$ export SPA_PLUGIN_PATH=$(pkg-config --variable plugindir libspa-0.2)
-$ spa-inspect ${SPA_PLUGIN_PATH}/support/libspa-support.so
+$ export SPA_PLUGIN_DIR=$(pkg-config --variable plugindir libspa-0.2)
+$ spa-inspect ${SPA_PLUGIN_DIR}/support/libspa-support.so
 ...
 factory version:		1
 factory name:		'support.cpu'
@@ -87,11 +87,11 @@ later, instead of hardcoding the plugin name.
 To `dlopen` a plugin we then need to prefix the plugin path like this:
 
 \code{.c}
-#define SPA_PLUGIN_PATH	/usr/lib64/spa-0.2/"
-void *hnd = dlopen(SPA_PLUGIN_PATH"/support/libspa-support.so", RTLD_NOW);
+#define SPA_PLUGIN_DIR	/usr/lib64/spa-0.2/"
+void *hnd = dlopen(SPA_PLUGIN_DIR"/support/libspa-support.so", RTLD_NOW);
 \endcode
 
-The environment variable `SPA_PLUGIN_PATH` and `pkg-config` variable
+The environment variable `SPA_PLUGIN_DIR` and `pkg-config` variable
 `plugindir` are usually used to find the location of the plugins. You will
 have to do some more work to construct the shared object path.
 
diff --git a/meson.build b/meson.build
index 5420c10edf1d7f5d4c523d09c4f3999ec1a201af..448725e59bd6bfabe996df5d955fe7016de4fc6e 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
 project('pipewire', ['c' ],
-  version : '0.3.61',
+  version : '0.3.63',
   license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ],
   meson_version : '>= 0.59.0',
   default_options : [ 'warning_level=3',
@@ -301,6 +301,9 @@ summary({'GLib-2.0 (Flatpak support)': glib2_dep.found()}, bool_yn: true, sectio
 flatpak_support = glib2_dep.found()
 cdata.set('HAVE_GLIB2', flatpak_support)
 
+gio_dep = dependency('gio-2.0', version : '>= 2.26.0', required : get_option('gsettings'))
+summary({'GIO (GSettings)': gio_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+
 gst_option = get_option('gstreamer')
 gst_deps_def = {
   'glib-2.0': {'version': '>=2.32.0'},
diff --git a/meson_options.txt b/meson_options.txt
index f306ecbf92449ae98b273e963467cb9cc4699330..fd5c9dab21efa5028653f7da946d2fb01475286e 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -177,9 +177,9 @@ option('videotestsrc',
        type: 'feature',
        value: 'enabled')
 option('volume',
-       description: 'Enable volume spa plugin integration',
+       description: 'Build the legacy volume spa plugin',
        type: 'feature',
-       value: 'enabled')
+       value: 'disabled')
 option('vulkan',
        description: 'Enable vulkan spa plugin integration',
        type: 'feature',
@@ -269,3 +269,7 @@ option('readline',
        description: 'Enable code that depends on libreadline',
        type: 'feature',
        value: 'auto')
+option('gsettings',
+       description: 'Enable code that depends on gsettings',
+       type: 'feature',
+       value: 'auto')
diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c
index 98eede7de469ad72c999a1f40ea5977f661f2a49..ab837f60336549fa8ae050df42fc33ad850a325b 100644
--- a/pipewire-jack/src/pipewire-jack.c
+++ b/pipewire-jack/src/pipewire-jack.c
@@ -1002,6 +1002,38 @@ static inline void fix_midi_event(uint8_t *data, size_t size)
 	}
 }
 
+static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b)
+{
+	if (a->offset < b->offset)
+		return -1;
+	if (a->offset > b->offset)
+		return 1;
+	if (a->type != b->type)
+		return 0;
+	switch(a->type) {
+	case SPA_CONTROL_Midi:
+	{
+		/* 11 (controller) > 12 (program change) >
+		 * 8 (note off) > 9 (note on) > 10 (aftertouch) >
+		 * 13 (channel pressure) > 14 (pitch bend) */
+		static int priotab[] = { 5,4,3,7,6,2,1,0 };
+		uint8_t *da, *db;
+
+		if (SPA_POD_BODY_SIZE(&a->value) < 1 ||
+		    SPA_POD_BODY_SIZE(&b->value) < 1)
+			return 0;
+
+		da = SPA_POD_BODY(&a->value);
+		db = SPA_POD_BODY(&b->value);
+		if ((da[0] & 0xf) != (db[0] & 0xf))
+			return 0;
+		return priotab[(db[0]>>4) & 7] - priotab[(da[0]>>4) & 7];
+	}
+	default:
+		return 0;
+	}
+}
+
 static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void *midi, bool fix)
 {
 	struct spa_pod_control *c[n_seq];
@@ -1014,15 +1046,13 @@ static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void
 	while (true) {
 		struct spa_pod_control *next = NULL;
 		uint32_t next_index = 0;
-		uint8_t *data;
-		size_t size;
 
 		for (i = 0; i < n_seq; i++) {
 			if (!spa_pod_control_is_inside(&seq[i]->body,
 						SPA_POD_BODY_SIZE(seq[i]), c[i]))
 				continue;
 
-			if (next == NULL || c[i]->offset < next->offset) {
+			if (next == NULL || event_sort(c[i], next) <= 0) {
 				next = c[i];
 				next_index = i;
 			}
@@ -1030,11 +1060,12 @@ static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void
 		if (SPA_UNLIKELY(next == NULL))
 			break;
 
-		data = SPA_POD_BODY(&next->value);
-		size = SPA_POD_BODY_SIZE(&next->value);
-
 		switch(next->type) {
 		case SPA_CONTROL_Midi:
+		{
+			uint8_t *data = SPA_POD_BODY(&next->value);
+			size_t size = SPA_POD_BODY_SIZE(&next->value);
+
 			if (fix)
 				fix_midi_event(data, size);
 
@@ -1043,6 +1074,7 @@ static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void
 						spa_strerror(res));
 			break;
 		}
+		}
 		c[next_index] = spa_pod_control_next(c[next_index]);
 	}
 }
@@ -1144,6 +1176,8 @@ static void complete_process(struct client *c, uint32_t frames)
                 if (pw_map_item_is_free(item))
 			continue;
 		p = item->data;
+		if (!p->valid)
+			continue;
 		spa_list_for_each(mix, &p->mix, port_link) {
 			if (SPA_LIKELY(mix->io != NULL))
 				mix->io->status = SPA_STATUS_NEED_DATA;
@@ -1153,6 +1187,8 @@ static void complete_process(struct client *c, uint32_t frames)
                 if (pw_map_item_is_free(item))
 			continue;
 		p = item->data;
+		if (!p->valid)
+			continue;
 		prepare_output(p, frames);
 		p->io.status = SPA_STATUS_NEED_DATA;
 	}
diff --git a/spa/include/spa/buffer/meta.h b/spa/include/spa/buffer/meta.h
index e270c56cacbaf13d9d9a93b5f1cc2ae127b067f4..ec09f18bd2b6bfff2a427f41c35f68da1b94364a 100644
--- a/spa/include/spa/buffer/meta.h
+++ b/spa/include/spa/buffer/meta.h
@@ -39,16 +39,17 @@ extern "C" {
 
 enum spa_meta_type {
 	SPA_META_Invalid,
-	SPA_META_Header,	/**< struct spa_meta_header */
-	SPA_META_VideoCrop,	/**< struct spa_meta_region with cropping data */
-	SPA_META_VideoDamage,	/**< array of struct spa_meta_region with damage, where an invalid entry or end-of-array marks the end. */
-	SPA_META_Bitmap,	/**< struct spa_meta_bitmap */
-	SPA_META_Cursor,	/**< struct spa_meta_cursor */
-	SPA_META_Control,	/**< metadata contains a spa_meta_control
-				  *  associated with the data */
-	SPA_META_Busy,		/**< don't write to buffer when count > 0 */
-
-	_SPA_META_LAST,		/**< not part of ABI/API */
+	SPA_META_Header,		/**< struct spa_meta_header */
+	SPA_META_VideoCrop,		/**< struct spa_meta_region with cropping data */
+	SPA_META_VideoDamage,		/**< array of struct spa_meta_region with damage, where an invalid entry or end-of-array marks the end. */
+	SPA_META_Bitmap,		/**< struct spa_meta_bitmap */
+	SPA_META_Cursor,		/**< struct spa_meta_cursor */
+	SPA_META_Control,		/**< metadata contains a spa_meta_control
+					  *  associated with the data */
+	SPA_META_Busy,			/**< don't write to buffer when count > 0 */
+	SPA_META_VideoTransform,	/**< struct spa_meta_transform */
+
+	_SPA_META_LAST,			/**< not part of ABI/API */
 };
 
 /**
@@ -161,6 +162,24 @@ struct spa_meta_busy {
 	uint32_t count;			/**< number of users busy with the buffer */
 };
 
+enum spa_meta_videotransform_value {
+	SPA_META_TRANSFORMATION_None = 0,	/**< no transform */
+	SPA_META_TRANSFORMATION_90,		/**< 90 degree counter-clockwise */
+	SPA_META_TRANSFORMATION_180,		/**< 180 degree counter-clockwise */
+	SPA_META_TRANSFORMATION_270,		/**< 270 degree counter-clockwise */
+	SPA_META_TRANSFORMATION_Flipped,	/**< 180 degree flipped around the vertical axis. Equivalent
+						  * to a reflexion through the vertical line splitting the
+						  * bufffer in two equal sized parts */
+	SPA_META_TRANSFORMATION_Flipped90,	/**< flip then rotate around 90 degree counter-clockwise */
+	SPA_META_TRANSFORMATION_Flipped180,	/**< flip then rotate around 180 degree counter-clockwise */
+	SPA_META_TRANSFORMATION_Flipped270,	/**< flip then rotate around 270 degree counter-clockwise */
+};
+
+/** a transformation of the buffer */
+struct spa_meta_videotransform {
+	uint32_t transform;		/**< orientation transformation that was applied to the buffer */
+};
+
 /**
  * \}
  */
diff --git a/spa/include/spa/param/props.h b/spa/include/spa/param/props.h
index 3f57bdeb6a0441396ee8e5f7c13eba945522b6a6..900dffaeb904d4f62ac65588a71f35971799c47d 100644
--- a/spa/include/spa/param/props.h
+++ b/spa/include/spa/param/props.h
@@ -74,6 +74,7 @@ enum spa_prop {
 	SPA_PROP_rate,
 	SPA_PROP_quality,
 	SPA_PROP_bluetoothAudioCodec,
+	SPA_PROP_bluetoothOffloadActive,
 
 	SPA_PROP_START_Audio	= 0x10000,	/**< audio related properties */
 	SPA_PROP_waveType,
diff --git a/spa/include/spa/param/type-info.h b/spa/include/spa/param/type-info.h
index 55a03124e2d2be814183f6574e990eb41527253d..ecde2371ba02ccdd7360612c74af08405376ff62 100644
--- a/spa/include/spa/param/type-info.h
+++ b/spa/include/spa/param/type-info.h
@@ -117,6 +117,7 @@ static const struct spa_type_info spa_type_props[] = {
 	{ SPA_PROP_rate, SPA_TYPE_Double, SPA_TYPE_INFO_PROPS_BASE "rate", NULL },
 	{ SPA_PROP_quality, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "quality", NULL },
 	{ SPA_PROP_bluetoothAudioCodec, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "bluetoothAudioCodec", spa_type_bluetooth_audio_codec },
+	{ SPA_PROP_bluetoothOffloadActive, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "bluetoothOffloadActive", NULL },
 
 	{ SPA_PROP_waveType, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "waveType", NULL },
 	{ SPA_PROP_frequency, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "frequency", NULL },
diff --git a/spa/plugins/aec/aec-webrtc.cpp b/spa/plugins/aec/aec-webrtc.cpp
index 5c129c534b5c2ee5fa17c9bc24a2c601fdb2a2e3..19a506e84c5b573caa0573d72a9c278e8551125f 100644
--- a/spa/plugins/aec/aec-webrtc.cpp
+++ b/spa/plugins/aec/aec-webrtc.cpp
@@ -100,7 +100,8 @@ static int webrtc_init(void *object, const struct spa_dict *args, const struct s
 	}
 
 	apm->high_pass_filter()->Enable(high_pass_filter);
-	// Always disable drift compensation since it requires drift sampling
+	// Always disable drift compensation since PipeWire will already do
+	// drift compensation on all sinks and sources linked to this echo-canceler
 	apm->echo_cancellation()->enable_drift_compensation(false);
 	apm->echo_cancellation()->Enable(true);
 	// TODO: wire up supression levels to args
diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c
index 4bbe5ee927e0eb29e34768beb381dae555ac133a..b969c3d89aab40d2d753d8f38144b43ccb832c65 100644
--- a/spa/plugins/alsa/acp/acp.c
+++ b/spa/plugins/alsa/acp/acp.c
@@ -34,7 +34,7 @@ void *_acp_log_data;
 
 struct spa_i18n *acp_i18n;
 
-#define DEFAULT_RATE	44100
+#define DEFAULT_RATE	48000
 
 #define VOLUME_ACCURACY (PA_VOLUME_NORM/100)  /* don't require volume adjustments to be perfectly correct. don't necessarily extend granularity in software unless the differences get greater than this level */
 
@@ -1348,7 +1348,6 @@ static int device_disable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_devic
 static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device *dev)
 {
 	const char *mod_name;
-	bool ignore_dB = false;
 	uint32_t i, port_index;
 	int res;
 
@@ -1365,7 +1364,7 @@ static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device
 
 	dev->device.flags |= ACP_DEVICE_ACTIVE;
 
-	find_mixer(impl, dev, NULL, ignore_dB);
+	find_mixer(impl, dev, NULL, impl->ignore_dB);
 
 	/* Synchronize priority values, as it may have changed when setting the profile */
 	for (i = 0; i < impl->card.n_ports; i++) {
@@ -1386,7 +1385,7 @@ static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device
 	if (dev->active_port)
 		dev->active_port->port.flags |= ACP_PORT_ACTIVE;
 
-	if ((res = setup_mixer(impl, dev, ignore_dB)) < 0)
+	if ((res = setup_mixer(impl, dev, impl->ignore_dB)) < 0)
 		return res;
 
 	if (dev->read_volume)
@@ -1533,7 +1532,6 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
 	struct acp_card *card;
 	const char *s, *profile_set = NULL, *profile = NULL;
 	char device_id[16];
-	bool ignore_dB = false;
 	uint32_t profile_index;
 	int res;
 
@@ -1554,6 +1552,7 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
 	impl->use_ucm = true;
 	impl->auto_profile = true;
 	impl->auto_port = true;
+	impl->ignore_dB = false;
 
 	if (props) {
 		if ((s = acp_dict_lookup(props, "api.alsa.use-ucm")) != NULL)
@@ -1561,7 +1560,7 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
 		if ((s = acp_dict_lookup(props, "api.alsa.soft-mixer")) != NULL)
 			impl->soft_mixer = spa_atob(s);
 		if ((s = acp_dict_lookup(props, "api.alsa.ignore-dB")) != NULL)
-			ignore_dB = spa_atob(s);
+			impl->ignore_dB = spa_atob(s);
 		if ((s = acp_dict_lookup(props, "device.profile-set")) != NULL)
 			profile_set = s;
 		if ((s = acp_dict_lookup(props, "device.profile")) != NULL)
@@ -1609,7 +1608,7 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
 		goto error;
 	}
 
-	impl->profile_set->ignore_dB = ignore_dB;
+	impl->profile_set->ignore_dB = impl->ignore_dB;
 
 	pa_alsa_profile_set_probe(impl->profile_set, impl->ucm.mixers,
 			device_id,
diff --git a/spa/plugins/alsa/acp/card.h b/spa/plugins/alsa/acp/card.h
index 139e2a6ae2e5c7c144fe11330965dd2c4dd94211..9f58343325faea01ea0d07d7320ccd0450235aab 100644
--- a/spa/plugins/alsa/acp/card.h
+++ b/spa/plugins/alsa/acp/card.h
@@ -46,6 +46,7 @@ struct pa_card {
 	bool soft_mixer;
 	bool auto_profile;
 	bool auto_port;
+	bool ignore_dB;
 
 	pa_alsa_ucm_config ucm;
 	pa_alsa_profile_set *profile_set;
diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c
index 5e0a60b375e8658f8d4c682543393c6e6c9c0292..76fe433b8354816ea0ed35c0843744024de59280 100644
--- a/spa/plugins/alsa/alsa-pcm.c
+++ b/spa/plugins/alsa/alsa-pcm.c
@@ -1583,11 +1583,18 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_
 	if (is_batch)
 		state->headroom += period_size;
 
+	if (spa_strstartswith(state->props.device, "a52") ||
+	    spa_strstartswith(state->props.device, "dca"))
+		state->min_delay = SPA_MIN(2048u, state->buffer_frames);
+	else
+		state->min_delay = 0;
+
 	state->headroom = SPA_MIN(state->headroom, state->buffer_frames);
 	state->start_delay = state->default_start_delay;
 
-	state->latency[state->port_direction].min_rate = state->headroom;
-	state->latency[state->port_direction].max_rate = state->headroom;
+	state->latency[state->port_direction].min_rate =
+		state->latency[state->port_direction].max_rate =
+			SPA_MAX(state->min_delay, state->headroom);
 
 	spa_log_info(state->log, "%s (%s): format:%s access:%s-%s rate:%d channels:%d "
 			"buffer frames %lu, period frames %lu, periods %u, frame_size %zd "
@@ -1859,7 +1866,7 @@ static int get_status(struct state *state, uint64_t current_time,
 		*delay = avail;
 		*target = SPA_MAX(*target, state->read_size);
 	}
-	*target = SPA_MIN(*target, state->buffer_frames);
+	*target = SPA_CLAMP(*target, state->min_delay, state->buffer_frames);
 	return 0;
 }
 
@@ -2428,9 +2435,20 @@ static void alsa_on_timeout_event(struct spa_source *source)
 	struct state *state = source->data;
 	snd_pcm_uframes_t delay, target;
 	uint64_t expire, current_time;
+	int res;
 
-	if (SPA_UNLIKELY(state->started && spa_system_timerfd_read(state->data_system, state->timerfd, &expire) < 0))
-		spa_log_warn(state->log, "%p: error reading timerfd: %m", state);
+	if (SPA_LIKELY(state->started)) {
+		if (SPA_UNLIKELY((res = spa_system_timerfd_read(state->data_system,
+						state->timerfd, &expire)) < 0)) {
+			/* we can get here when the timer is changed since the last
+			 * timerfd wakeup, for example by do_reassign_follower() executed
+			 * in the same epoll wakeup cycle */
+			if (res != -EAGAIN)
+				spa_log_warn(state->log, "%p: error reading timerfd: %s",
+						state, spa_strerror(res));
+			return;
+		}
+	}
 
 	check_position_config(state);
 
@@ -2610,6 +2628,8 @@ int spa_alsa_reassign_follower(struct state *state)
 		else
 			snd_pcm_pause(state->hndl, 0);
 	}
+
+	state->alsa_sync_warning = false;
 	return 0;
 }
 
diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h
index c630de3acc349b5fc919f72c602cabefa4997a82..9c4a86862563e03ecfce690cf707e529d361cef6 100644
--- a/spa/plugins/alsa/alsa-pcm.h
+++ b/spa/plugins/alsa/alsa-pcm.h
@@ -197,6 +197,7 @@ struct state {
 	uint32_t last_threshold;
 	uint32_t headroom;
 	uint32_t start_delay;
+	uint32_t min_delay;
 
 	uint32_t duration;
 	unsigned int alsa_started:1;
diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c
index 407b88f84cb08a85f464a5e6856583cd5f9f04b8..9cec44d2d4463b614b5a4234ab9383ac9f3aa550 100644
--- a/spa/plugins/alsa/alsa-seq.c
+++ b/spa/plugins/alsa/alsa-seq.c
@@ -800,8 +800,14 @@ static void alsa_on_timeout_event(struct spa_source *source)
 	uint64_t expire;
 	int res;
 
-	if (state->started && spa_system_timerfd_read(state->data_system, state->timerfd, &expire) < 0)
-		spa_log_warn(state->log, "error reading timerfd: %m");
+	if (state->started) {
+		if ((res = spa_system_timerfd_read(state->data_system, state->timerfd, &expire)) < 0) {
+			if (res != -EAGAIN)
+				spa_log_warn(state->log, "%p: error reading timerfd: %s",
+						state, spa_strerror(res));
+			return;
+		}
+	}
 
 	state->current_time = state->next_time;
 
diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c
index 14bf3e6d970f926d34f36cee4b39018697939e2f..6875264465308aa331bddfd681e080219047d5d5 100644
--- a/spa/plugins/audioconvert/audioadapter.c
+++ b/spa/plugins/audioconvert/audioadapter.c
@@ -138,6 +138,27 @@ static int follower_enum_params(struct impl *this,
 	return 0;
 }
 
+static int convert_enum_port_config(struct impl *this,
+		int seq, uint32_t id, uint32_t start, uint32_t num,
+		const struct spa_pod *filter, struct spa_pod_builder *builder)
+{
+	struct spa_pod *f1, *f2 = NULL;
+	int res;
+
+	f1 = spa_pod_builder_add_object(builder,
+		SPA_TYPE_OBJECT_ParamPortConfig, id,
+			SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction));
+
+	if (filter) {
+		if ((res = spa_pod_filter(builder, &f2, f1, filter)) < 0)
+			return res;
+	}
+	else {
+		f2 = f1;
+	}
+	return spa_node_enum_params(this->convert, seq, id, start, num, f2);
+}
+
 static int impl_node_enum_params(void *object, int seq,
 				 uint32_t id, uint32_t start, uint32_t num,
 				 const struct spa_pod *filter)
@@ -163,9 +184,25 @@ next:
 
 	switch (id) {
 	case SPA_PARAM_EnumPortConfig:
+		return convert_enum_port_config(this, seq, id, start, num, filter, &b.b);
 	case SPA_PARAM_PortConfig:
-		res = spa_node_enum_params(this->convert, seq, id, start, num, filter);
-		return res;
+		if (this->passthrough) {
+			switch (result.index) {
+			case 0:
+				result.param = spa_pod_builder_add_object(&b.b,
+					SPA_TYPE_OBJECT_ParamPortConfig, id,
+					SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction),
+					SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(
+						SPA_PARAM_PORT_CONFIG_MODE_passthrough));
+				result.next++;
+				break;
+			default:
+				return 0;
+			}
+		} else {
+			return convert_enum_port_config(this, seq, id, start, num, filter, &b.b);
+		}
+		break;
 	case SPA_PARAM_PropInfo:
 		res = follower_enum_params(this,
 				id, IDX_PropInfo, &result, filter, &b.b);
@@ -436,6 +473,7 @@ static int negotiate_buffers(struct impl *this)
 
 static int configure_format(struct impl *this, uint32_t flags, const struct spa_pod *format)
 {
+	uint8_t buffer[4096];
 	int res;
 
 	if (format == NULL && !this->have_format)
@@ -450,14 +488,13 @@ static int configure_format(struct impl *this, uint32_t flags, const struct spa_
 					   SPA_PARAM_Format, flags,
 					   format)) < 0)
 			return res;
+
 	if (res > 0) {
-		uint8_t buffer[4096];
-		struct spa_pod_builder b = { 0 };
+		struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
 		uint32_t state = 0;
 		struct spa_pod *fmt;
 
 		/* format was changed to nearest compatible format */
-		spa_pod_builder_init(&b, buffer, sizeof(buffer));
 
 		if ((res = spa_node_port_enum_params_sync(this->follower,
 					this->direction, 0,
@@ -813,9 +850,11 @@ static int impl_node_send_command(void *object, const struct spa_command *comman
 		this->started = true;
 		break;
 	case SPA_NODE_COMMAND_Suspend:
+		this->started = false;
 		spa_log_debug(this->log, "%p: suspending", this);
 		break;
 	case SPA_NODE_COMMAND_Pause:
+		this->started = false;
 		spa_log_debug(this->log, "%p: pausing", this);
 		break;
 	case SPA_NODE_COMMAND_Flush:
@@ -847,10 +886,10 @@ static int impl_node_send_command(void *object, const struct spa_command *comman
 		break;
 	case SPA_NODE_COMMAND_Suspend:
 		configure_format(this, 0, NULL);
-		SPA_FALLTHROUGH
+		spa_log_debug(this->log, "%p: suspended", this);
+		break;
 	case SPA_NODE_COMMAND_Pause:
-		this->started = false;
-		spa_log_debug(this->log, "%p: stopped", this);
+		spa_log_debug(this->log, "%p: paused", this);
 		break;
 	case SPA_NODE_COMMAND_Flush:
 		spa_log_debug(this->log, "%p: flushed", this);
@@ -1164,7 +1203,7 @@ static int follower_ready(void *data, int status)
 	spa_log_trace_fp(this->log, "%p: ready %d", this, status);
 
 	if (!this->started) {
-		spa_log_warn(this->log, "%p: ready stopped node", this);
+		spa_log_info(this->log, "%p: ready stopped node", this);
 		return -EIO;
 	}
 
diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c
index 8e5237bdbf29bb2a0b7e87a8b2fd4ac91fcc478e..ccf4e30c5fb898a6afc77670a8c473520bde9824 100644
--- a/spa/plugins/audioconvert/audioconvert.c
+++ b/spa/plugins/audioconvert/audioconvert.c
@@ -161,6 +161,7 @@ struct dir {
 	struct port *ports[MAX_PORTS];
 	uint32_t n_ports;
 
+	enum spa_direction direction;
 	enum spa_param_port_config_mode mode;
 
 	struct spa_audio_info format;
@@ -378,55 +379,61 @@ static int impl_node_enum_params(void *object, int seq,
 
 	switch (id) {
 	case SPA_PARAM_EnumPortConfig:
+	{
+		struct dir *dir;
 		switch (result.index) {
 		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamPortConfig, id,
-				SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT),
-				SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp));
+			dir = &this->dir[SPA_DIRECTION_INPUT];;
 			break;
 		case 1:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamPortConfig, id,
-				SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
-				SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp));
-			break;
-		case 2:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamPortConfig, id,
-				SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT),
-				SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_convert));
-			break;
-		case 3:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamPortConfig, id,
-				SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
-				SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_convert));
+			dir = &this->dir[SPA_DIRECTION_OUTPUT];;
 			break;
 		default:
 			return 0;
 		}
+		param = spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_ParamPortConfig, id,
+			SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction),
+			SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_CHOICE_ENUM_Id(4,
+				SPA_PARAM_PORT_CONFIG_MODE_none,
+				SPA_PARAM_PORT_CONFIG_MODE_none,
+				SPA_PARAM_PORT_CONFIG_MODE_dsp,
+				SPA_PARAM_PORT_CONFIG_MODE_convert),
+			SPA_PARAM_PORT_CONFIG_monitor,   SPA_POD_CHOICE_Bool(false),
+			SPA_PARAM_PORT_CONFIG_control,   SPA_POD_CHOICE_Bool(false));
 		break;
-
+	}
 	case SPA_PARAM_PortConfig:
+	{
+		struct dir *dir;
+		struct spa_pod_frame f[1];
+
 		switch (result.index) {
 		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamPortConfig, id,
-				SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT),
-				SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(this->dir[SPA_DIRECTION_INPUT].mode));
+			dir = &this->dir[SPA_DIRECTION_INPUT];;
 			break;
 		case 1:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamPortConfig, id,
-				SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
-				SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(this->dir[SPA_DIRECTION_OUTPUT].mode));
+			dir = &this->dir[SPA_DIRECTION_OUTPUT];;
 			break;
 		default:
 			return 0;
 		}
+		spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_ParamPortConfig, id);
+		spa_pod_builder_add(&b,
+			SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction),
+			SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(dir->mode),
+			SPA_PARAM_PORT_CONFIG_monitor,   SPA_POD_Bool(this->monitor),
+			SPA_PARAM_PORT_CONFIG_control,   SPA_POD_Bool(dir->control),
+			0);
+
+		if (dir->have_format) {
+			spa_pod_builder_prop(&b, SPA_PARAM_PORT_CONFIG_format, 0);
+			spa_format_audio_raw_build(&b, SPA_PARAM_PORT_CONFIG_format,
+					&dir->format.info.raw);
+		}
+		param = spa_pod_builder_pop(&b, &f[0]);
 		break;
-
+	}
 	case SPA_PARAM_PropInfo:
 	{
 		struct props *p = &this->props;
@@ -1015,6 +1022,8 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m
 		init_port(this, direction, 0, 0, false, false, false);
 		break;
 	}
+	case SPA_PARAM_PORT_CONFIG_MODE_none:
+		break;
 	default:
 		return -ENOTSUP;
 	}
@@ -1169,45 +1178,6 @@ static int setup_in_convert(struct impl *this)
 	return 0;
 }
 
-#define _MASK(ch)	(1ULL << SPA_AUDIO_CHANNEL_ ## ch)
-#define STEREO	(_MASK(FL)|_MASK(FR))
-
-static uint64_t default_mask(uint32_t channels)
-{
-	uint64_t mask = 0;
-	switch (channels) {
-	case 7:
-	case 8:
-		mask |= _MASK(RL);
-		mask |= _MASK(RR);
-		SPA_FALLTHROUGH
-	case 5:
-	case 6:
-		mask |= _MASK(SL);
-		mask |= _MASK(SR);
-		if ((channels & 1) == 0)
-			mask |= _MASK(LFE);
-		SPA_FALLTHROUGH
-	case 3:
-		mask |= _MASK(FC);
-		SPA_FALLTHROUGH
-	case 2:
-		mask |= _MASK(FL);
-		mask |= _MASK(FR);
-		break;
-	case 1:
-		mask |= _MASK(MONO);
-		break;
-	case 4:
-		mask |= _MASK(FL);
-		mask |= _MASK(FR);
-		mask |= _MASK(RL);
-		mask |= _MASK(RR);
-		break;
-	}
-	return mask;
-}
-
 static void fix_volumes(struct impl *this, struct volumes *vols, uint32_t channels)
 {
 	float s;
@@ -1328,11 +1298,6 @@ static int setup_channelmix(struct impl *this)
 	spa_log_info(this->log, "out %s (%016"PRIx64")", format_position(str, sizeof(str),
 				dst_chan, out->format.info.raw.position), dst_mask);
 
-	if (src_mask & 1)
-		src_mask = default_mask(src_chan);
-	if (dst_mask & 1)
-		dst_mask = default_mask(dst_chan);
-
 	spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d %08"PRIx64":%08"PRIx64, this,
 			spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32),
 			src_chan,
@@ -2870,7 +2835,7 @@ impl_init(const struct spa_handle_factory *factory,
 
 	props_reset(&this->props);
 
-	this->mix.options = CHANNELMIX_OPTION_UPMIX;
+	this->mix.options = CHANNELMIX_OPTION_UPMIX | CHANNELMIX_OPTION_MIX_LFE;
 	this->mix.upmix = CHANNELMIX_UPMIX_PSD;
 	this->mix.log = this->log;
 	this->mix.lfe_cutoff = 150.0f;
@@ -2906,7 +2871,9 @@ impl_init(const struct spa_handle_factory *factory,
 	this->props.soft.n_volumes = this->props.n_channels;
 	this->props.monitor.n_volumes = this->props.n_channels;
 
+	this->dir[SPA_DIRECTION_INPUT].direction = SPA_DIRECTION_INPUT;
 	this->dir[SPA_DIRECTION_INPUT].latency = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+	this->dir[SPA_DIRECTION_OUTPUT].direction = SPA_DIRECTION_OUTPUT;
 	this->dir[SPA_DIRECTION_OUTPUT].latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
 
 	this->node.iface = SPA_INTERFACE_INIT(
diff --git a/spa/plugins/audioconvert/channelmix-ops.c b/spa/plugins/audioconvert/channelmix-ops.c
index cfc449f0b0d739d0292ef6d68229c68e98817aa4..44d3967761e3270f9261b60b3b8bce87c61033ca 100644
--- a/spa/plugins/audioconvert/channelmix-ops.c
+++ b/spa/plugins/audioconvert/channelmix-ops.c
@@ -522,6 +522,7 @@ done:
 
 			if (i == 0)
 				idx2 += snprintf(str2 + idx2, sizeof(str2) - idx2, "%-4.4s  ",
+						src_mask == ~0LU ? "MONO" :
 						spa_debug_type_find_short_name(spa_type_audio_channel, j + 3));
 
 			mix->matrix_orig[ic][jc++] = matrix[i][j];
@@ -536,6 +537,7 @@ done:
 			if (i == 0)
 				spa_log_info(mix->log, "     %s", str2);
 			spa_log_info(mix->log, "%-4.4s %s   %f",
+					dst_mask == ~0LU ? "MONO" :
 					spa_debug_type_find_short_name(spa_type_audio_channel, i + 3),
 					str, sum);
 		}
diff --git a/spa/plugins/audioconvert/fmt-ops-avx2.c b/spa/plugins/audioconvert/fmt-ops-avx2.c
index 723aea369bc67f5959c7e5b99fe30707d902464e..087f0275ec4d631609160bda336e9850ee37f22c 100644
--- a/spa/plugins/audioconvert/fmt-ops-avx2.c
+++ b/spa/plugins/audioconvert/fmt-ops-avx2.c
@@ -156,11 +156,12 @@ void
 conv_s24_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
 {
-	const int24_t *s = src;
+	const int8_t *s = src;
 	float *d0 = dst[0];
 	uint32_t n, unrolled;
 	__m128i in;
 	__m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE);
+	__m128i mask1 = _mm_setr_epi32(0*n_channels, 3*n_channels, 6*n_channels, 9*n_channels);
 
 	if (SPA_IS_ALIGNED(d0, 16) && n_samples > 0) {
 		unrolled = n_samples & ~3;
@@ -171,23 +172,19 @@ conv_s24_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 4) {
-		in = _mm_setr_epi32(
-			*((uint32_t*)&s[0 * n_channels]),
-			*((uint32_t*)&s[1 * n_channels]),
-			*((uint32_t*)&s[2 * n_channels]),
-			*((uint32_t*)&s[3 * n_channels]));
+		in = _mm_i32gather_epi32((int*)s, mask1, 1);
 		in = _mm_slli_epi32(in, 8);
 		in = _mm_srai_epi32(in, 8);
 		out = _mm_cvtepi32_ps(in);
 		out = _mm_mul_ps(out, factor);
 		_mm_store_ps(&d0[n], out);
-		s += 4 * n_channels;
+		s += 12 * n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
+		out = _mm_cvtsi32_ss(factor, s24_to_s32(*(int24_t*)s));
 		out = _mm_mul_ss(out, factor);
 		_mm_store_ss(&d0[n], out);
-		s += n_channels;
+		s += 3 * n_channels;
 	}
 }
 
@@ -195,11 +192,12 @@ static void
 conv_s24_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
 {
-	const int24_t *s = src;
+	const int8_t *s = src;
 	float *d0 = dst[0], *d1 = dst[1];
 	uint32_t n, unrolled;
 	__m128i in[2];
 	__m128 out[2], factor = _mm_set1_ps(1.0f / S24_SCALE);
+	__m128i mask1 = _mm_setr_epi32(0*n_channels, 3*n_channels, 6*n_channels, 9*n_channels);
 
 	if (SPA_IS_ALIGNED(d0, 16) &&
 	    SPA_IS_ALIGNED(d1, 16) &&
@@ -212,16 +210,8 @@ conv_s24_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 4) {
-		in[0] = _mm_setr_epi32(
-			*((uint32_t*)&s[0 + 0*n_channels]),
-			*((uint32_t*)&s[0 + 1*n_channels]),
-			*((uint32_t*)&s[0 + 2*n_channels]),
-			*((uint32_t*)&s[0 + 3*n_channels]));
-		in[1] = _mm_setr_epi32(
-			*((uint32_t*)&s[1 + 0*n_channels]),
-			*((uint32_t*)&s[1 + 1*n_channels]),
-			*((uint32_t*)&s[1 + 2*n_channels]),
-			*((uint32_t*)&s[1 + 3*n_channels]));
+		in[0] = _mm_i32gather_epi32((int*)&s[0], mask1, 1);
+		in[1] = _mm_i32gather_epi32((int*)&s[3], mask1, 1);
 
 		in[0] = _mm_slli_epi32(in[0], 8);
 		in[1] = _mm_slli_epi32(in[1], 8);
@@ -238,27 +228,28 @@ conv_s24_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		_mm_store_ps(&d0[n], out[0]);
 		_mm_store_ps(&d1[n], out[1]);
 
-		s += 4 * n_channels;
+		s += 12 * n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
-		out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1)));
+		out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+0)));
+		out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+1)));
 		out[0] = _mm_mul_ss(out[0], factor);
 		out[1] = _mm_mul_ss(out[1], factor);
 		_mm_store_ss(&d0[n], out[0]);
 		_mm_store_ss(&d1[n], out[1]);
-		s += n_channels;
+		s += 3 * n_channels;
 	}
 }
 static void
 conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
 {
-	const int24_t *s = src;
+	const int8_t *s = src;
 	float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3];
 	uint32_t n, unrolled;
 	__m128i in[4];
 	__m128 out[4], factor = _mm_set1_ps(1.0f / S24_SCALE);
+	__m128i mask1 = _mm_setr_epi32(0*n_channels, 3*n_channels, 6*n_channels, 9*n_channels);
 
 	if (SPA_IS_ALIGNED(d0, 16) &&
 	    SPA_IS_ALIGNED(d1, 16) &&
@@ -273,26 +264,10 @@ conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 4) {
-		in[0] = _mm_setr_epi32(
-			*((uint32_t*)&s[0 + 0*n_channels]),
-			*((uint32_t*)&s[0 + 1*n_channels]),
-			*((uint32_t*)&s[0 + 2*n_channels]),
-			*((uint32_t*)&s[0 + 3*n_channels]));
-		in[1] = _mm_setr_epi32(
-			*((uint32_t*)&s[1 + 0*n_channels]),
-			*((uint32_t*)&s[1 + 1*n_channels]),
-			*((uint32_t*)&s[1 + 2*n_channels]),
-			*((uint32_t*)&s[1 + 3*n_channels]));
-		in[2] = _mm_setr_epi32(
-			*((uint32_t*)&s[2 + 0*n_channels]),
-			*((uint32_t*)&s[2 + 1*n_channels]),
-			*((uint32_t*)&s[2 + 2*n_channels]),
-			*((uint32_t*)&s[2 + 3*n_channels]));
-		in[3] = _mm_setr_epi32(
-			*((uint32_t*)&s[3 + 0*n_channels]),
-			*((uint32_t*)&s[3 + 1*n_channels]),
-			*((uint32_t*)&s[3 + 2*n_channels]),
-			*((uint32_t*)&s[3 + 3*n_channels]));
+		in[0] = _mm_i32gather_epi32((int*)&s[0], mask1, 1);
+		in[1] = _mm_i32gather_epi32((int*)&s[3], mask1, 1);
+		in[2] = _mm_i32gather_epi32((int*)&s[6], mask1, 1);
+		in[3] = _mm_i32gather_epi32((int*)&s[9], mask1, 1);
 
 		in[0] = _mm_slli_epi32(in[0], 8);
 		in[1] = _mm_slli_epi32(in[1], 8);
@@ -319,13 +294,13 @@ conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		_mm_store_ps(&d2[n], out[2]);
 		_mm_store_ps(&d3[n], out[3]);
 
-		s += 4 * n_channels;
+		s += 12 * n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
-		out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1)));
-		out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+2)));
-		out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+3)));
+		out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+0)));
+		out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+1)));
+		out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+2)));
+		out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+3)));
 		out[0] = _mm_mul_ss(out[0], factor);
 		out[1] = _mm_mul_ss(out[1], factor);
 		out[2] = _mm_mul_ss(out[2], factor);
@@ -334,7 +309,7 @@ conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		_mm_store_ss(&d1[n], out[1]);
 		_mm_store_ss(&d2[n], out[2]);
 		_mm_store_ss(&d3[n], out[3]);
-		s += n_channels;
+		s += 3 * n_channels;
 	}
 }
 
@@ -361,12 +336,10 @@ conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	const int32_t *s = src;
 	float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3];
 	uint32_t n, unrolled;
-	__m256i in[4], t[4];
+	__m256i in[4];
 	__m256 out[4], factor = _mm256_set1_ps(1.0f / S24_SCALE);
-	__m256i mask1 = _mm256_setr_epi64x(0*n_channels, 0*n_channels+2, 4*n_channels, 4*n_channels+2);
-	__m256i mask2 = _mm256_setr_epi64x(1*n_channels, 1*n_channels+2, 5*n_channels, 5*n_channels+2);
-	__m256i mask3 = _mm256_setr_epi64x(2*n_channels, 2*n_channels+2, 6*n_channels, 6*n_channels+2);
-	__m256i mask4 = _mm256_setr_epi64x(3*n_channels, 3*n_channels+2, 7*n_channels, 7*n_channels+2);
+	__m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels,
+					  4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels);
 
 	if (SPA_IS_ALIGNED(d0, 32) &&
 	    SPA_IS_ALIGNED(d1, 32) &&
@@ -377,19 +350,10 @@ conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 8) {
-		in[0] = _mm256_i64gather_epi64((long long int *)&s[0*n_channels], mask1, 4);
-		in[1] = _mm256_i64gather_epi64((long long int *)&s[0*n_channels], mask2, 4);
-		in[2] = _mm256_i64gather_epi64((long long int *)&s[0*n_channels], mask3, 4);
-		in[3] = _mm256_i64gather_epi64((long long int *)&s[0*n_channels], mask4, 4);
-
-		t[0] = _mm256_unpacklo_epi32(in[0], in[1]);   /* a0 a1 b0 b1 a4 a5 b4 b5 */
-		t[1] = _mm256_unpackhi_epi32(in[0], in[1]);   /* c0 c1 d0 d1 c4 c5 d4 d5 */
-		t[2] = _mm256_unpacklo_epi32(in[2], in[3]);   /* a2 a3 b2 b3 a6 a7 b6 b7 */
-		t[3] = _mm256_unpackhi_epi32(in[2], in[3]);   /* c2 c3 d2 d3 c6 c7 d6 d7 */
-		in[0] = _mm256_unpacklo_epi64(t[0], t[2]);     /* a0 a1 a2 a3 a4 a5 a6 a7 */
-		in[1] = _mm256_unpackhi_epi64(t[0], t[2]);     /* b0 b1 b2 b3 b4 b5 b6 b7 */
-		in[2] = _mm256_unpacklo_epi64(t[1], t[3]);     /* c0 c1 c2 c3 c4 c5 c6 c7 */
-		in[3] = _mm256_unpackhi_epi64(t[1], t[3]);     /* d0 d1 d2 d3 d4 d5 d6 d7 */
+		in[0] = _mm256_i32gather_epi32((int*)&s[0], mask1, 4);
+		in[1] = _mm256_i32gather_epi32((int*)&s[1], mask1, 4);
+		in[2] = _mm256_i32gather_epi32((int*)&s[2], mask1, 4);
+		in[3] = _mm256_i32gather_epi32((int*)&s[3], mask1, 4);
 
 		in[0] = _mm256_srai_epi32(in[0], 8);
 		in[1] = _mm256_srai_epi32(in[1], 8);
@@ -438,11 +402,10 @@ conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	const int32_t *s = src;
 	float *d0 = dst[0], *d1 = dst[1];
 	uint32_t n, unrolled;
-	__m256i in[4], t[4];
+	__m256i in[4];
 	__m256 out[4], factor = _mm256_set1_ps(1.0f / S24_SCALE);
-	__m256i perm = _mm256_setr_epi32(0, 2, 4, 6, 1, 3, 5, 7);
-	__m256i mask1 = _mm256_setr_epi64x(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels);
-	__m256i mask2 = _mm256_setr_epi64x(4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels);
+	__m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels,
+					  4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels);
 
 	if (SPA_IS_ALIGNED(d0, 32) &&
 	    SPA_IS_ALIGNED(d1, 32))
@@ -451,14 +414,8 @@ conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 8) {
-		in[0] = _mm256_i64gather_epi64((long long int *)s, mask1, 4);
-		in[1] = _mm256_i64gather_epi64((long long int *)s, mask2, 4);
-
-		t[0] = _mm256_permutevar8x32_epi32(in[0], perm);
-		t[1] = _mm256_permutevar8x32_epi32(in[1], perm);
-
-		in[0] = _mm256_permute2x128_si256(t[0], t[1], 0 | (2 << 4));
-		in[1] = _mm256_permute2x128_si256(t[0], t[1], 1 | (3 << 4));
+		in[0] = _mm256_i32gather_epi32((int*)&s[0], mask1, 4);
+		in[1] = _mm256_i32gather_epi32((int*)&s[1], mask1, 4);
 
 		in[0] = _mm256_srai_epi32(in[0], 8);
 		in[1] = _mm256_srai_epi32(in[1], 8);
@@ -495,8 +452,8 @@ conv_s32_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	uint32_t n, unrolled;
 	__m256i in[2];
 	__m256 out[2], factor = _mm256_set1_ps(1.0f / S24_SCALE);
-	__m256i mask1 = _mm256_setr_epi64x(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels);
-	__m256i mask2 = _mm256_setr_epi64x(4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels);
+	__m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels,
+					  4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels);
 
 	if (SPA_IS_ALIGNED(d0, 32))
 		unrolled = n_samples & ~15;
@@ -504,12 +461,8 @@ conv_s32_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 16) {
-		in[0] = _mm256_setr_m128i(
-				_mm256_i64gather_epi32(&s[ 0*n_channels], mask1, 4),
-				_mm256_i64gather_epi32(&s[ 0*n_channels], mask2, 4));
-		in[1] = _mm256_setr_m128i(
-				_mm256_i64gather_epi32(&s[ 8*n_channels], mask1, 4),
-				_mm256_i64gather_epi32(&s[ 8*n_channels], mask2, 4));
+		in[0] = _mm256_i32gather_epi32(&s[0*n_channels], mask1, 4);
+		in[1] = _mm256_i32gather_epi32(&s[8*n_channels], mask1, 4);
 
 		in[0] = _mm256_srai_epi32(in[0], 8);
 		in[1] = _mm256_srai_epi32(in[1], 8);
diff --git a/spa/plugins/audiotestsrc/audiotestsrc.c b/spa/plugins/audiotestsrc/audiotestsrc.c
index f009b9a29b5a40c78c32d5c7852c3317fb7cabd6..c0c9c7a68dbc9daf3f042f8f9236e1d572a9a3f9 100644
--- a/spa/plugins/audiotestsrc/audiotestsrc.c
+++ b/spa/plugins/audiotestsrc/audiotestsrc.c
@@ -34,6 +34,7 @@
 #include <spa/support/loop.h>
 #include <spa/utils/list.h>
 #include <spa/utils/keys.h>
+#include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/node/node.h>
 #include <spa/node/utils.h>
@@ -348,14 +349,20 @@ static void set_timer(struct impl *this, bool enabled)
 	}
 }
 
-static void read_timer(struct impl *this)
+static int read_timer(struct impl *this)
 {
 	uint64_t expirations;
+	int res = 0;
 
 	if (this->async || this->props.live) {
-		if (spa_system_timerfd_read(this->data_system, this->timer_source.fd, &expirations) < 0)
-			perror("read timerfd");
+		if ((res = spa_system_timerfd_read(this->data_system,
+				this->timer_source.fd, &expirations)) < 0) {
+			if (res != -EAGAIN)
+				spa_log_error(this->log, NAME " %p: timerfd error: %s",
+						this, spa_strerror(res));
+		}
 	}
+	return 0;
 }
 
 static int make_buffer(struct impl *this)
@@ -369,7 +376,8 @@ static int make_buffer(struct impl *this)
 	uint32_t filled, avail;
 	uint32_t index, offset, l0, l1;
 
-	read_timer(this);
+	if (read_timer(this) < 0)
+		return 0;
 
 	if (spa_list_is_empty(&port->empty)) {
 		set_timer(this, false);
diff --git a/spa/plugins/avb/avb-pcm.c b/spa/plugins/avb/avb-pcm.c
index 8fe9503a379be7f684d7907a6cc29a9c47b95698..484adc6605ee42ad6c20bf2a5273d3d3be675a2a 100644
--- a/spa/plugins/avb/avb-pcm.c
+++ b/spa/plugins/avb/avb-pcm.c
@@ -36,6 +36,7 @@
 #include <arpa/inet.h>
 
 #include <spa/pod/filter.h>
+#include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/support/system.h>
 #include <spa/utils/keys.h>
@@ -1048,14 +1049,15 @@ static void avb_on_timeout_event(struct spa_source *source)
 	struct state *state = source->data;
 	uint64_t expirations, current_time, duration;
 	uint32_t rate;
+	int res;
 
 	spa_log_trace(state->log, "timeout");
 
-	if (spa_system_timerfd_read(state->data_system,
-				state->timer_source.fd, &expirations) < 0) {
-		if (errno == EAGAIN)
-			return;
-		spa_log_error(state->log, "read timerfd: %m");
+	if ((res = spa_system_timerfd_read(state->data_system,
+				state->timer_source.fd, &expirations)) < 0) {
+		if (res != -EAGAIN)
+			spa_log_error(state->log, "read timerfd: %s", spa_strerror(res));
+		return;
 	}
 
 	current_time = state->next_time;
diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c
index f3c2d721f3b05f031f3d6284d1785047b52de317..558cff2bee8918e15f03054621169144e210d167 100644
--- a/spa/plugins/bluez5/bluez5-device.c
+++ b/spa/plugins/bluez5/bluez5-device.c
@@ -79,11 +79,13 @@ enum {
 
 struct props {
 	enum spa_bluetooth_audio_codec codec;
+	bool offload_active;
 };
 
 static void reset_props(struct props *props)
 {
 	props->codec = 0;
+	props->offload_active = false;
 }
 
 struct impl;
@@ -97,6 +99,7 @@ struct node {
 	unsigned int mute:1;
 	unsigned int save:1;
 	unsigned int a2dp_duplex:1;
+	unsigned int offload_acquired:1;
 	uint32_t n_channels;
 	int64_t latency_offset;
 	uint32_t channels[SPA_AUDIO_MAX_CHANNELS];
@@ -407,6 +410,24 @@ static const struct spa_bt_transport_events transport_events = {
 	.volume_changed = volume_changed,
 };
 
+static int node_offload_set_active(struct node *node, bool active)
+{
+	int res = 0;
+
+	if (node->transport == NULL || !node->active)
+		return -ENOTSUP;
+
+	if (active && !node->offload_acquired)
+		res = spa_bt_transport_acquire(node->transport, false);
+	else if (!active && node->offload_acquired)
+		res = spa_bt_transport_release(node->transport);
+
+	if (res >= 0)
+		node->offload_acquired = active;
+
+	return res;
+}
+
 static void get_channels(struct spa_bt_transport *t, bool a2dp_duplex, uint32_t *n_channels, uint32_t *channels)
 {
 	const struct media_codec *codec;
@@ -480,6 +501,7 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t,
 
 		this->nodes[id].impl = this;
 		this->nodes[id].active = true;
+		this->nodes[id].offload_acquired = false;
 		this->nodes[id].a2dp_duplex = a2dp_duplex;
 		get_channels(t, a2dp_duplex, &this->nodes[id].n_channels, this->nodes[id].channels);
 		if (this->nodes[id].transport)
@@ -804,6 +826,7 @@ static void emit_remove_nodes(struct impl *this)
 
 	for (uint32_t i = 0; i < 2; i++) {
 		struct node * node = &this->nodes[i];
+		node_offload_set_active(node, false);
 		if (node->transport) {
 			spa_hook_remove(&node->transport_listener);
 			node->transport = NULL;
@@ -813,6 +836,8 @@ static void emit_remove_nodes(struct impl *this)
 			node->active = false;
 		}
 	}
+
+	this->props.offload_active = false;
 }
 
 static bool validate_profile(struct impl *this, uint32_t profile,
@@ -1674,7 +1699,7 @@ next:
 	return true;
 }
 
-static struct spa_pod *build_prop_info(struct impl *this, struct spa_pod_builder *b, uint32_t id)
+static struct spa_pod *build_prop_info_codec(struct impl *this, struct spa_pod_builder *b, uint32_t id)
 {
 	struct spa_pod_frame f[2];
 	struct spa_pod_choice *choice;
@@ -1748,7 +1773,8 @@ static struct spa_pod *build_props(struct impl *this, struct spa_pod_builder *b,
 
 	return spa_pod_builder_add_object(b,
 			SPA_TYPE_OBJECT_Props, id,
-			SPA_PROP_bluetoothAudioCodec, SPA_POD_Id(p->codec));
+			SPA_PROP_bluetoothAudioCodec, SPA_POD_Id(p->codec),
+			SPA_PROP_bluetoothOffloadActive, SPA_POD_Bool(p->offload_active));
 }
 
 static int impl_enum_params(void *object, int seq,
@@ -1841,7 +1867,14 @@ static int impl_enum_params(void *object, int seq,
 	{
 		switch (result.index) {
 		case 0:
-			param = build_prop_info(this, &b, id);
+			param = build_prop_info_codec(this, &b, id);
+			break;
+		case 1:
+			param = spa_pod_builder_add_object(&b,
+					SPA_TYPE_OBJECT_PropInfo, id,
+					SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_bluetoothOffloadActive),
+					SPA_PROP_INFO_description, SPA_POD_String("Bluetooth audio offload active"),
+					SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(false));
 			break;
 		default:
 			return 0;
@@ -2030,6 +2063,25 @@ static int apply_device_props(struct impl *this, struct node *node, struct spa_p
 	return changed;
 }
 
+static void apply_prop_offload_active(struct impl *this, bool active)
+{
+	bool old_value = this->props.offload_active;
+
+	this->props.offload_active = active;
+
+	for (int i = 0; i < 2; i++) {
+		node_offload_set_active(&this->nodes[i], active);
+		if (!this->nodes[i].offload_acquired)
+			this->props.offload_active = false;
+	}
+
+	if (this->props.offload_active != old_value) {
+		this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+		this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+		emit_info(this, false);
+	}
+}
+
 static int impl_set_param(void *object,
 			  uint32_t id, uint32_t flags,
 			  const struct spa_pod *param)
@@ -2104,19 +2156,23 @@ static int impl_set_param(void *object,
 	case SPA_PARAM_Props:
 	{
 		uint32_t codec_id = SPA_ID_INVALID;
+		bool offload_active = this->props.offload_active;
 
 		if (param == NULL)
 			return 0;
 
 		if ((res = spa_pod_parse_object(param,
 				SPA_TYPE_OBJECT_Props, NULL,
-				SPA_PROP_bluetoothAudioCodec, SPA_POD_OPT_Id(&codec_id))) < 0) {
+				SPA_PROP_bluetoothAudioCodec, SPA_POD_OPT_Id(&codec_id),
+				SPA_PROP_bluetoothOffloadActive, SPA_POD_OPT_Bool(&offload_active))) < 0) {
 			spa_log_warn(this->log, "can't parse props");
 			spa_debug_pod(0, NULL, param);
 			return res;
 		}
 
-		spa_log_debug(this->log, "setting props codec:%d", codec_id);
+		spa_log_debug(this->log, "setting props codec:%d offload:%d", (int)codec_id, (int)offload_active);
+
+		apply_prop_offload_active(this, offload_active);
 
 		if (codec_id == SPA_ID_INVALID)
 			return 0;
diff --git a/spa/plugins/bluez5/media-sink.c b/spa/plugins/bluez5/media-sink.c
index a5d72fcfca7b0ff4ab67b538fccdf84b2f75d86a..ee6cef4ce8d2c8da2c0a56162d0233e0c293f689 100644
--- a/spa/plugins/bluez5/media-sink.c
+++ b/spa/plugins/bluez5/media-sink.c
@@ -839,11 +839,15 @@ static void media_on_flush_timeout(struct spa_source *source)
 {
 	struct impl *this = source->data;
 	uint64_t exp;
+	int res;
 
 	spa_log_trace(this->log, "%p: flush on timeout", this);
 
-	if (spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp) < 0)
-		spa_log_warn(this->log, "error reading timerfd: %s", strerror(errno));
+	if ((res = spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp)) < 0) {
+		if (res != -EAGAIN)
+			spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+		return;
+	}
 
 	if (this->transport == NULL) {
 		enable_flush_timer(this, false);
@@ -864,12 +868,19 @@ static void media_on_timeout(struct spa_source *source)
 	uint32_t rate;
 	struct spa_io_buffers *io = port->io;
 	uint64_t prev_time, now_time;
+	int res;
 
 	if (this->transport == NULL)
 		return;
 
-	if (this->started && spa_system_timerfd_read(this->data_system, this->timerfd, &exp) < 0)
-		spa_log_warn(this->log, "error reading timerfd: %s", strerror(errno));
+	if (this->started) {
+		if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+			if (res != -EAGAIN)
+				spa_log_warn(this->log, "error reading timerfd: %s",
+						spa_strerror(res));
+			return;
+		}
+	}
 
 	prev_time = this->current_time;
 	now_time = this->current_time = this->next_time;
diff --git a/spa/plugins/bluez5/media-source.c b/spa/plugins/bluez5/media-source.c
index 58ff14a52955a73d16bc019a14d391f417cd4d5b..d260335ce0251dbb96ea4ceaf1fc327bf0729222 100644
--- a/spa/plugins/bluez5/media-source.c
+++ b/spa/plugins/bluez5/media-source.c
@@ -538,9 +538,13 @@ static void media_on_duplex_timeout(struct spa_source *source)
 {
 	struct impl *this = source->data;
 	uint64_t exp;
+	int res;
 
-	if (spa_system_timerfd_read(this->data_system, this->duplex_timerfd, &exp) < 0)
-		spa_log_warn(this->log, "error reading timerfd: %s", strerror(errno));
+	if ((res = spa_system_timerfd_read(this->data_system, this->duplex_timerfd, &exp)) < 0) {
+		if (res != -EAGAIN)
+			spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+		return;
+	}
 
 	set_duplex_timeout(this, this->duplex_timeout);
 
@@ -577,12 +581,18 @@ static void media_on_timeout(struct spa_source *source)
 	uint64_t exp, duration;
 	uint32_t rate;
 	uint64_t prev_time, now_time;
+	int res;
 
 	if (this->transport == NULL)
 		return;
 
-	if (this->started && spa_system_timerfd_read(this->data_system, this->timerfd, &exp) < 0)
-		spa_log_warn(this->log, "error reading timerfd: %s", strerror(errno));
+	if (this->started) {
+		if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+			if (res != -EAGAIN)
+				spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+			return;
+		}
+	}
 
 	prev_time = this->current_time;
 	now_time = this->current_time = this->next_time;
diff --git a/spa/plugins/bluez5/sco-sink.c b/spa/plugins/bluez5/sco-sink.c
index f8db7eaf8fcf031610288dd0aab9d253dede3671..7025c78a9155c0746473e4222bf7077f8c9a5489 100644
--- a/spa/plugins/bluez5/sco-sink.c
+++ b/spa/plugins/bluez5/sco-sink.c
@@ -95,7 +95,6 @@ struct port {
 	struct buffer buffers[MAX_BUFFERS];
 	uint32_t n_buffers;
 
-	struct spa_list free;
 	struct spa_list ready;
 
 	struct buffer *current_buffer;
@@ -568,16 +567,19 @@ stop:
 	enable_flush_timer(this, false);
 }
 
-
 static void sco_on_flush_timeout(struct spa_source *source)
 {
 	struct impl *this = source->data;
 	uint64_t exp;
+	int res;
 
 	spa_log_trace(this->log, "%p: flush on timeout", this);
 
-	if (spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp) < 0)
-		spa_log_warn(this->log, "error reading timerfd: %s", strerror(errno));
+	if ((res = spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp)) < 0) {
+		if (res != -EAGAIN)
+			spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+		return;
+	}
 
 	if (this->transport == NULL) {
 		enable_flush_timer(this, false);
@@ -598,12 +600,18 @@ static void sco_on_timeout(struct spa_source *source)
 	uint32_t rate;
 	struct spa_io_buffers *io = port->io;
 	uint64_t prev_time, now_time;
+	int res;
 
 	if (this->transport == NULL)
 		return;
 
-	if (this->started && spa_system_timerfd_read(this->data_system, this->timerfd, &exp) < 0)
-		spa_log_warn(this->log, "error reading timerfd: %s", strerror(errno));
+	if (this->started) {
+		if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+			if (res != -EAGAIN)
+				spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+			return;
+		}
+	}
 
 	prev_time = this->current_time;
 	now_time = this->current_time = this->next_time;
diff --git a/spa/plugins/bluez5/sco-source.c b/spa/plugins/bluez5/sco-source.c
index 17855065f0f4634ecfebc6a58a49f672ec3038b8..b4c40983a769bcd60334ae4848da350933ddba54 100644
--- a/spa/plugins/bluez5/sco-source.c
+++ b/spa/plugins/bluez5/sco-source.c
@@ -34,6 +34,7 @@
 #include <spa/support/loop.h>
 #include <spa/support/log.h>
 #include <spa/support/system.h>
+#include <spa/utils/result.h>
 #include <spa/utils/list.h>
 #include <spa/utils/keys.h>
 #include <spa/utils/names.h>
@@ -600,12 +601,19 @@ static void sco_on_timeout(struct spa_source *source)
 	uint64_t exp, duration;
 	uint32_t rate;
 	uint64_t prev_time, now_time;
+	int res;
 
 	if (this->transport == NULL)
 		return;
 
-	if (this->started && spa_system_timerfd_read(this->data_system, this->timerfd, &exp) < 0)
-		spa_log_warn(this->log, "error reading timerfd: %s", strerror(errno));
+	if (this->started) {
+		if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+			if (res != -EAGAIN)
+				spa_log_warn(this->log, "error reading timerfd: %s",
+						spa_strerror(res));
+			return;
+		}
+	}
 
 	prev_time = this->current_time;
 	now_time = this->current_time = this->next_time;
diff --git a/spa/plugins/control/mixer.c b/spa/plugins/control/mixer.c
index 81a3bd5191cab114c76fe5b8fb0c8ddac84aabc9..1ef212b46ebc24b07745e1718de645a4cd8378f7 100644
--- a/spa/plugins/control/mixer.c
+++ b/spa/plugins/control/mixer.c
@@ -37,6 +37,7 @@
 #include <spa/node/io.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/param.h>
+#include <spa/control/control.h>
 #include <spa/pod/filter.h>
 
 #define NAME "control-mixer"
@@ -571,6 +572,38 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t
 	return queue_buffer(this, port, &port->buffers[buffer_id]);
 }
 
+static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b)
+{
+	if (a->offset < b->offset)
+		return -1;
+	if (a->offset > b->offset)
+		return 1;
+	if (a->type != b->type)
+		return 0;
+	switch(a->type) {
+	case SPA_CONTROL_Midi:
+	{
+		/* 11 (controller) > 12 (program change) >
+		 * 8 (note off) > 9 (note on) > 10 (aftertouch) >
+		 * 13 (channel pressure) > 14 (pitch bend) */
+		static int priotab[] = { 5,4,3,7,6,2,1,0 };
+		uint8_t *da, *db;
+
+		if (SPA_POD_BODY_SIZE(&a->value) < 1 ||
+		    SPA_POD_BODY_SIZE(&b->value) < 1)
+			return 0;
+
+		da = SPA_POD_BODY(&a->value);
+		db = SPA_POD_BODY(&b->value);
+		if ((da[0] & 0xf) != (db[0] & 0xf))
+			return 0;
+		return priotab[(db[0]>>4) & 7] - priotab[(da[0]>>4) & 7];
+	}
+	default:
+		return 0;
+	}
+}
+
 static int impl_node_process(void *object)
 {
 	struct impl *this = object;
@@ -664,7 +697,7 @@ static int impl_node_process(void *object)
 					SPA_POD_BODY_SIZE(seq[i]), ctrl[i]))
 				continue;
 
-			if (next == NULL || ctrl[i]->offset < next->offset) {
+			if (next == NULL || event_sort(ctrl[i], next) <= 0) {
 				next = ctrl[i];
 				next_index = i;
 			}
diff --git a/spa/plugins/libcamera/libcamera-source.cpp b/spa/plugins/libcamera/libcamera-source.cpp
index 565325369cd4b499b896a635660d20c9506e58f4..50954e5e3ddaa37a13284702b82395f02f8da54b 100644
--- a/spa/plugins/libcamera/libcamera-source.cpp
+++ b/spa/plugins/libcamera/libcamera-source.cpp
@@ -78,6 +78,7 @@ struct buffer {
 	struct spa_list link;
 	struct spa_buffer *outbuf;
 	struct spa_meta_header *h;
+	struct spa_meta_videotransform *videotransform;
 	void *ptr;
 };
 
@@ -589,6 +590,12 @@ next:
 				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
 				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
 			break;
+		case 1:
+			param = (struct spa_pod*)spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamMeta, id,
+				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoTransform),
+				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_videotransform)));
+			break;
 		default:
 			return 0;
 		}
diff --git a/spa/plugins/libcamera/libcamera-utils.cpp b/spa/plugins/libcamera/libcamera-utils.cpp
index 446220bd10500cb38035cd9bddc1e3f120f39010..af07484f2011998da6812c1b2bf1dbe6c66f1b41 100644
--- a/spa/plugins/libcamera/libcamera-utils.cpp
+++ b/spa/plugins/libcamera/libcamera-utils.cpp
@@ -74,7 +74,7 @@ static void spa_libcamera_get_config(struct impl *impl)
 		return;
 
 	StreamRoles roles;
-	roles.push_back(VideoRecording);
+	roles.push_back(StreamRole::VideoRecording);
 	impl->config = impl->camera->generateConfiguration(roles);
 }
 
@@ -500,28 +500,48 @@ next:
 			0);
 
 	switch (ctrl_id->type()) {
-	case ControlTypeBool:
+	case ControlTypeBool: {
+		bool def;
+		if (ctrl_info.def().isNone())
+			def = ctrl_info.min().get<bool>();
+		else
+			def = ctrl_info.def().get<bool>();
+
 		spa_pod_builder_add(&b,
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
-						(bool)ctrl_info.def().get<bool>()),
-				0);
-		break;
-	case ControlTypeFloat:
+					SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
+							def),
+					0);
+	} break;
+	case ControlTypeFloat: {
+		float min = ctrl_info.min().get<float>();
+		float max = ctrl_info.max().get<float>();
+		float def;
+
+		if (ctrl_info.def().isNone())
+			def = (min + max) / 2;
+		else
+			def = ctrl_info.def().get<float>();
+
 		spa_pod_builder_add(&b,
 				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
-						(float)ctrl_info.def().get<float>(),
-						(float)ctrl_info.min().get<float>(),
-						(float)ctrl_info.max().get<float>()),
+						def, min, max),
 				0);
-		break;
-	case ControlTypeInteger32:
+	} break;
+	case ControlTypeInteger32: {
+		int32_t min = ctrl_info.min().get<int32_t>();
+		int32_t max = ctrl_info.max().get<int32_t>();
+		int32_t def;
+
+		if (ctrl_info.def().isNone())
+			def = (min + max) / 2;
+		else
+			def = ctrl_info.def().get<int32_t>();
+
 		spa_pod_builder_add(&b,
 				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(
-						(int32_t)ctrl_info.def().get<int32_t>(),
-						(int32_t)ctrl_info.min().get<int32_t>(),
-						(int32_t)ctrl_info.max().get<int32_t>()),
+						def, min, max),
 				0);
-		break;
+	} break;
 	default:
 		goto next;
 	}
@@ -678,6 +698,31 @@ static int spa_libcamera_use_buffers(struct impl *impl, struct port *port,
 	return -ENOTSUP;
 }
 
+static const struct {
+	Transform libcamera_transform;
+	uint32_t spa_transform_value;
+} transform_map[] = {
+	{ Transform::Identity, SPA_META_TRANSFORMATION_None },
+	{ Transform::Rot0, SPA_META_TRANSFORMATION_None },
+	{ Transform::HFlip, SPA_META_TRANSFORMATION_Flipped },
+	{ Transform::VFlip, SPA_META_TRANSFORMATION_Flipped180 },
+	{ Transform::HVFlip, SPA_META_TRANSFORMATION_180 },
+	{ Transform::Rot180, SPA_META_TRANSFORMATION_180 },
+	{ Transform::Transpose, SPA_META_TRANSFORMATION_Flipped90 },
+	{ Transform::Rot90, SPA_META_TRANSFORMATION_90 },
+	{ Transform::Rot270, SPA_META_TRANSFORMATION_270 },
+	{ Transform::Rot180Transpose, SPA_META_TRANSFORMATION_Flipped270 },
+};
+
+static uint32_t libcamera_transform_to_spa_transform_value(Transform transform)
+{
+	for (const auto& t : transform_map) {
+		if (t.libcamera_transform == transform)
+			return t.spa_transform_value;
+	}
+	return SPA_META_TRANSFORMATION_None;
+}
+
 static int
 mmap_init(struct impl *impl, struct port *port,
 		struct spa_buffer **buffers, uint32_t n_buffers)
@@ -722,6 +767,16 @@ mmap_init(struct impl *impl, struct port *port,
 		b->flags = BUFFER_FLAG_OUTSTANDING;
 		b->h = (struct spa_meta_header*)spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
 
+		b->videotransform = (struct spa_meta_videotransform*)spa_buffer_find_meta_data(
+			buffers[i], SPA_META_VideoTransform, sizeof(*b->videotransform));
+		if (b->videotransform) {
+			b->videotransform->transform =
+				libcamera_transform_to_spa_transform_value(impl->config->transform);
+			spa_log_debug(impl->log, "Setting videotransform for buffer %d to %u (from %s)",
+				i, b->videotransform->transform, transformToString(impl->config->transform));
+
+		}
+
 		d = buffers[i]->datas;
 		for(j = 0; j < buffers[i]->n_datas; ++j) {
 			d[j].type = port->memtype;
diff --git a/spa/plugins/support/loop.c b/spa/plugins/support/loop.c
index dd308cfda99a3445c0eeba1c9f358fbf1595a34e..3c3e020eefa68f1aa38fa80fe8384bbe048bf4d6 100644
--- a/spa/plugins/support/loop.c
+++ b/spa/plugins/support/loop.c
@@ -89,7 +89,7 @@ struct impl {
 	uint8_t *buffer_data;
 	uint8_t buffer_mem[DATAS_SIZE + MAX_ALIGN];
 
-	unsigned int flushing:1;
+	uint32_t flush_count;
 	unsigned int polling:1;
 };
 
@@ -166,23 +166,39 @@ static int loop_remove_source(void *object, struct spa_source *source)
 
 static void flush_items(struct impl *impl)
 {
-	uint32_t index;
+	uint32_t index, flush_count;
+	int32_t avail;
 	int res;
 
-	impl->flushing = true;
-	while (spa_ringbuffer_get_read_index(&impl->buffer, &index) > 0) {
+	flush_count = ++impl->flush_count;
+	avail = spa_ringbuffer_get_read_index(&impl->buffer, &index);
+	while (avail > 0) {
 		struct invoke_item *item;
 		bool block;
+		spa_invoke_func_t func;
 
 		item = SPA_PTROFF(impl->buffer_data, index & (DATAS_SIZE - 1), struct invoke_item);
 		block = item->block;
+		func = item->func;
 
 		spa_log_trace_fp(impl->log, "%p: flush item %p", impl, item);
-		item->res = item->func ? item->func(&impl->loop,
-				true, item->seq, item->data, item->size,
-			   item->user_data) : 0;
-
-		spa_ringbuffer_read_update(&impl->buffer, index + item->item_size);
+		/* first we remove the function from the item so that recursive
+		 * calls don't call the callback again. We can't update the
+		 * read index before we call the function because then the item
+		 * might get overwritten. */
+		item->func = NULL;
+		if (func)
+			item->res = func(&impl->loop, true, item->seq, item->data,
+				item->size, item->user_data);
+
+		/* if this function did a recursive invoke, it now flushed the
+		 * ringbuffer and we can exit */
+		if (flush_count != impl->flush_count)
+			break;
+
+		index += item->item_size;
+		avail -= item->item_size;
+		spa_ringbuffer_read_update(&impl->buffer, index);
 
 		if (block) {
 			if ((res = spa_system_eventfd_write(impl->system, impl->ack_fd, 1)) < 0)
@@ -190,20 +206,21 @@ static void flush_items(struct impl *impl)
 						impl, impl->ack_fd, spa_strerror(res));
 		}
 	}
-	impl->flushing = false;
 }
 
 static int
 loop_invoke_inthread(struct impl *impl,
-	    spa_invoke_func_t func,
-	    uint32_t seq,
-	    const void *data,
-	    size_t size,
-	    bool block,
-	    void *user_data)
+		spa_invoke_func_t func,
+		uint32_t seq,
+		const void *data,
+		size_t size,
+		bool block,
+		void *user_data)
 {
-	if (!impl->flushing)
-		flush_items(impl);
+	/* we should probably have a second ringbuffer for the in-thread pending
+	 * callbacks. A recursive callback when flushing will insert itself
+	 * before this one. */
+	flush_items(impl);
 	return func ? func(&impl->loop, true, seq, data, size, user_data) : 0;
 }
 
@@ -222,6 +239,9 @@ loop_invoke(void *object,
 	int32_t filled;
 	uint32_t avail, idx, offset, l0;
 
+	/* the ringbuffer can only be written to from one thread, if we are
+	 * in the same thread as the loop, don't write into the ringbuffer
+	 * but try to emit the calback right away after flushing what we have */
 	if (impl->thread == 0 || pthread_equal(impl->thread, pthread_self()))
 		return loop_invoke_inthread(impl, func, seq, data, size, block, user_data);
 
@@ -247,6 +267,7 @@ loop_invoke(void *object,
 	item->size = size;
 	item->block = block;
 	item->user_data = user_data;
+	item->res = 0;
 	item->item_size = SPA_ROUND_UP_N(sizeof(struct invoke_item) + size, ITEM_ALIGN);
 
 	spa_log_trace_fp(impl->log, "%p: add item %p filled:%d", impl, item, filled);
@@ -585,10 +606,12 @@ static void source_event_func(struct spa_source *source)
 	uint64_t count = 0;
 	int res;
 
-	if ((res = spa_system_eventfd_read(s->impl->system, source->fd, &count)) < 0)
-		spa_log_warn(s->impl->log, "%p: failed to read event fd:%d: %s",
-				source, source->fd, spa_strerror(res));
-
+	if ((res = spa_system_eventfd_read(s->impl->system, source->fd, &count)) < 0) {
+		if (res != -EAGAIN)
+			spa_log_warn(s->impl->log, "%p: failed to read event fd:%d: %s",
+					source, source->fd, spa_strerror(res));
+		return;
+	}
 	s->func.event(source->data, count);
 }
 
@@ -651,10 +674,12 @@ static void source_timer_func(struct spa_source *source)
 	int res;
 
 	if (SPA_UNLIKELY((res = spa_system_timerfd_read(s->impl->system,
-				source->fd, &expirations)) < 0))
-		spa_log_warn(s->impl->log, "%p: failed to read timer fd:%d: %s",
-				source, source->fd, spa_strerror(res));
-
+				source->fd, &expirations)) < 0)) {
+		if (res != -EAGAIN)
+			spa_log_warn(s->impl->log, "%p: failed to read timer fd:%d: %s",
+					source, source->fd, spa_strerror(res));
+		return;
+	}
 	s->func.timer(source->data, expirations);
 }
 
@@ -731,10 +756,12 @@ static void source_signal_func(struct spa_source *source)
 	struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source);
 	int res, signal_number = 0;
 
-	if ((res = spa_system_signalfd_read(s->impl->system, source->fd, &signal_number)) < 0)
-		spa_log_warn(s->impl->log, "%p: failed to read signal fd:%d: %s",
-				source, source->fd, spa_strerror(res));
-
+	if ((res = spa_system_signalfd_read(s->impl->system, source->fd, &signal_number)) < 0) {
+		if (res != -EAGAIN)
+			spa_log_warn(s->impl->log, "%p: failed to read signal fd:%d: %s",
+					source, source->fd, spa_strerror(res));
+		return;
+	}
 	s->func.signal(source->data, signal_number);
 }
 
diff --git a/spa/plugins/support/node-driver.c b/spa/plugins/support/node-driver.c
index 7fb6027ba668c7712e7e70380ba296f98fab4764..9701a478c1ff3b65188411278ecec0bb9fb7246e 100644
--- a/spa/plugins/support/node-driver.c
+++ b/spa/plugins/support/node-driver.c
@@ -32,6 +32,7 @@
 #include <spa/support/log.h>
 #include <spa/support/loop.h>
 #include <spa/utils/names.h>
+#include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/node/node.h>
 #include <spa/node/keys.h>
@@ -180,14 +181,16 @@ static void on_timeout(struct spa_source *source)
 	struct impl *this = source->data;
 	uint64_t expirations, nsec, duration;
 	uint32_t rate;
+	int res;
 
 	spa_log_trace(this->log, "timeout");
 
-	if (spa_system_timerfd_read(this->data_system,
-				this->timer_source.fd, &expirations) < 0) {
-		if (errno == EAGAIN)
-			return;
-		perror("read timerfd");
+	if ((res = spa_system_timerfd_read(this->data_system,
+				this->timer_source.fd, &expirations)) < 0) {
+		if (res != EAGAIN)
+			spa_log_error(this->log, NAME " %p: timerfd error: %s",
+					this, spa_strerror(res));
+		return;
 	}
 
 	nsec = this->next_time;
diff --git a/spa/plugins/support/null-audio-sink.c b/spa/plugins/support/null-audio-sink.c
index e42c3c1c8ecf7891c746f4b7311f0e9344ee2098..c39c15afbe8ac26f8bbaf4d6fda496072f7a8a3b 100644
--- a/spa/plugins/support/null-audio-sink.c
+++ b/spa/plugins/support/null-audio-sink.c
@@ -35,6 +35,7 @@
 #include <spa/utils/list.h>
 #include <spa/utils/keys.h>
 #include <spa/utils/json.h>
+#include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/node/node.h>
 #include <spa/node/utils.h>
@@ -282,14 +283,16 @@ static void on_timeout(struct spa_source *source)
 	struct impl *this = source->data;
 	uint64_t expirations, nsec, duration = 10;
 	uint32_t rate;
+	int res;
 
 	spa_log_trace(this->log, "timeout");
 
-	if (spa_system_timerfd_read(this->data_system,
-				this->timer_source.fd, &expirations) < 0) {
-		if (errno == EAGAIN)
-			return;
-		perror("read timerfd");
+	if ((res = spa_system_timerfd_read(this->data_system,
+				this->timer_source.fd, &expirations)) < 0) {
+		if (res != EAGAIN)
+			spa_log_error(this->log, NAME " %p: timerfd error: %s",
+					this, spa_strerror(res));
+		return;
 	}
 
 	nsec = this->next_time;
diff --git a/spa/plugins/test/fakesink.c b/spa/plugins/test/fakesink.c
index 104f27eb9da639e30c1d4f57ae7aaf6e277e4695..3c42b223924345a77fc0db5decdf94bb6253a47e 100644
--- a/spa/plugins/test/fakesink.c
+++ b/spa/plugins/test/fakesink.c
@@ -32,6 +32,7 @@
 #include <spa/support/log.h>
 #include <spa/support/loop.h>
 #include <spa/utils/list.h>
+#include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/node/node.h>
 #include <spa/node/utils.h>
@@ -215,15 +216,20 @@ static void set_timer(struct impl *this, bool enabled)
 	}
 }
 
-static inline void read_timer(struct impl *this)
+static inline int read_timer(struct impl *this)
 {
 	uint64_t expirations;
+	int res = 0;
 
 	if (this->callbacks.funcs || this->props.live) {
-		if (spa_system_timerfd_read(this->data_system,
-					this->timer_source.fd, &expirations) < 0)
-			perror("read timerfd");
+		if ((res = spa_system_timerfd_read(this->data_system,
+					this->timer_source.fd, &expirations)) < 0) {
+			if (res != -EAGAIN)
+				spa_log_error(this->log, NAME " %p: timerfd error: %s",
+						this, spa_strerror(res));
+		}
 	}
+	return res;
 }
 
 static void render_buffer(struct impl *this, struct buffer *b)
@@ -237,7 +243,8 @@ static int consume_buffer(struct impl *this)
 	struct spa_io_buffers *io = port->io;
 	int n_bytes;
 
-	read_timer(this);
+	if (read_timer(this) < 0)
+		return 0;
 
 	if (spa_list_is_empty(&port->ready)) {
 		io->status = SPA_STATUS_NEED_DATA;
diff --git a/spa/plugins/test/fakesrc.c b/spa/plugins/test/fakesrc.c
index 6dde68455d1c56d68f7ed7e482a3c87f808f9b9f..d4965a98b28f1cd64e94094c349095617d5f264e 100644
--- a/spa/plugins/test/fakesrc.c
+++ b/spa/plugins/test/fakesrc.c
@@ -32,6 +32,7 @@
 #include <spa/support/log.h>
 #include <spa/support/loop.h>
 #include <spa/utils/list.h>
+#include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/node/node.h>
 #include <spa/node/utils.h>
@@ -230,15 +231,20 @@ static void set_timer(struct impl *this, bool enabled)
 	}
 }
 
-static inline void read_timer(struct impl *this)
+static inline int read_timer(struct impl *this)
 {
 	uint64_t expirations;
+	int res = 0;
 
 	if (this->callbacks.funcs || this->props.live) {
-		if (spa_system_timerfd_read(this->data_system,
-					this->timer_source.fd, &expirations) < 0)
-			perror("read timerfd");
+		if ((res = spa_system_timerfd_read(this->data_system,
+					this->timer_source.fd, &expirations)) < 0) {
+			if (res != -EAGAIN)
+				spa_log_error(this->log, NAME " %p: timerfd error: %s",
+						this, spa_strerror(res));
+		}
 	}
+	return res;
 }
 
 static int make_buffer(struct impl *this)
@@ -248,7 +254,8 @@ static int make_buffer(struct impl *this)
 	struct spa_io_buffers *io = port->io;
 	int n_bytes;
 
-	read_timer(this);
+	if (read_timer(this) < 0)
+		return 0;
 
 	if (spa_list_is_empty(&port->empty)) {
 		set_timer(this, false);
diff --git a/spa/plugins/videotestsrc/videotestsrc.c b/spa/plugins/videotestsrc/videotestsrc.c
index f2f42c0609689ccf9ae931e48d1b8b88aff31a27..af3bc9688ffcafd1dd567f04cf94283680fa1a0f 100644
--- a/spa/plugins/videotestsrc/videotestsrc.c
+++ b/spa/plugins/videotestsrc/videotestsrc.c
@@ -33,6 +33,7 @@
 #include <spa/support/loop.h>
 #include <spa/utils/list.h>
 #include <spa/utils/keys.h>
+#include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/node/node.h>
 #include <spa/node/utils.h>
@@ -280,14 +281,20 @@ static void set_timer(struct impl *this, bool enabled)
 	}
 }
 
-static void read_timer(struct impl *this)
+static int read_timer(struct impl *this)
 {
 	uint64_t expirations;
+	int res = 0;
 
 	if (this->async || this->props.live) {
-		if (spa_system_timerfd_read(this->data_system, this->timer_source.fd, &expirations) < 0)
-			perror("read timerfd");
+		if ((res = spa_system_timerfd_read(this->data_system,
+						this->timer_source.fd, &expirations)) < 0) {
+			if (res != -EAGAIN)
+				spa_log_error(this->log, NAME " %p: timerfd error: %s",
+						this, spa_strerror(res));
+		}
 	}
+	return res;
 }
 
 static int make_buffer(struct impl *this)
@@ -297,7 +304,8 @@ static int make_buffer(struct impl *this)
 	struct spa_io_buffers *io = port->io;
 	uint32_t n_bytes;
 
-	read_timer(this);
+	if (read_timer(this) < 0)
+		return 0;
 
 	if (spa_list_is_empty(&port->empty)) {
 		set_timer(this, false);
diff --git a/spa/plugins/vulkan/vulkan-compute-source.c b/spa/plugins/vulkan/vulkan-compute-source.c
index 8cce5489f6475570203f2497dfaa751866c8af16..dade5a5d35f8a043f5ce0e951c9394500a205e87 100644
--- a/spa/plugins/vulkan/vulkan-compute-source.c
+++ b/spa/plugins/vulkan/vulkan-compute-source.c
@@ -34,6 +34,7 @@
 #include <spa/utils/list.h>
 #include <spa/utils/keys.h>
 #include <spa/utils/names.h>
+#include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/node/node.h>
 #include <spa/node/utils.h>
@@ -267,14 +268,20 @@ static void set_timer(struct impl *this, bool enabled)
 	}
 }
 
-static void read_timer(struct impl *this)
+static int read_timer(struct impl *this)
 {
 	uint64_t expirations;
+	int res = 0;
 
 	if (this->async || this->props.live) {
-		if (spa_system_timerfd_read(this->data_system, this->timer_source.fd, &expirations) < 0)
-			perror("read timerfd");
+		if ((res = spa_system_timerfd_read(this->data_system,
+						this->timer_source.fd, &expirations)) < 0) {
+			if (res != -EAGAIN)
+				spa_log_error(this->log, NAME " %p: timerfd error: %s",
+						this, spa_strerror(res));
+		}
 	}
+	return res;
 }
 
 static int make_buffer(struct impl *this)
@@ -284,7 +291,8 @@ static int make_buffer(struct impl *this)
 	uint32_t n_bytes;
 	int res;
 
-	read_timer(this);
+	if (read_timer(this) < 0)
+		return 0;
 
 	if ((res = spa_vulkan_ready(&this->state)) < 0) {
 		res = SPA_STATUS_OK;
diff --git a/src/daemon/client-rt.conf.in b/src/daemon/client-rt.conf.in
index f62b1e2281054af0e073362ff8ab4789645130ac..ba46bd421afd66740450eb981d7138a4aea51d79 100644
--- a/src/daemon/client-rt.conf.in
+++ b/src/daemon/client-rt.conf.in
@@ -82,7 +82,7 @@ stream.properties = {
     #node.autoconnect      = true
     #resample.quality      = 4
     #channelmix.normalize  = false
-    #channelmix.mix-lfe    = false
+    #channelmix.mix-lfe    = true
     #channelmix.upmix      = true
     #channelmix.upmix-method = psd  # none, simple
     #channelmix.lfe-cutoff = 150
diff --git a/src/daemon/client.conf.in b/src/daemon/client.conf.in
index 3931149f7e842bcaf6f7af8556eeafd1cadbee79..b465eb6445cb5d9bf78048cd0c983490ca64c83e 100644
--- a/src/daemon/client.conf.in
+++ b/src/daemon/client.conf.in
@@ -73,7 +73,7 @@ stream.properties = {
     #node.autoconnect      = true
     #resample.quality      = 4
     #channelmix.normalize  = false
-    #channelmix.mix-lfe    = false
+    #channelmix.mix-lfe    = true
     #channelmix.upmix      = true
     #channelmix.upmix-method = psd  # none, simple
     #channelmix.lfe-cutoff = 150
diff --git a/src/daemon/minimal.conf.in b/src/daemon/minimal.conf.in
index 8de1f21e1f76677a1acd2ce078997470d2fb65c2..4a3a2cb0740295a44112a5b8663ba27e51bb5bee 100644
--- a/src/daemon/minimal.conf.in
+++ b/src/daemon/minimal.conf.in
@@ -203,7 +203,7 @@ context.objects = [
             resample.disable       = true
             #monitor.channel-volumes = false
             #channelmix.normalize   = false
-            #channelmix.mix-lfe     = false
+            #channelmix.mix-lfe     = true
             #channelmix.upmix       = true
             #channelmix.upmix-method = psd  # none, simple
             #channelmix.lfe-cutoff  = 150
@@ -265,7 +265,7 @@ context.objects = [
             #resample.quality      = 4
             resample.disable      = true
             #channelmix.normalize  = false
-            #channelmix.mix-lfe    = false
+            #channelmix.mix-lfe    = true
             #channelmix.upmix      = true
             #channelmix.upmix-method = psd  # none, simple
             #channelmix.lfe-cutoff = 150
diff --git a/src/daemon/pipewire-avb.conf.in b/src/daemon/pipewire-avb.conf.in
index b4e465f07082948d767784c8ae3670567d4f177f..68f89ca4c915270037a2a967aee3b67f74781944 100644
--- a/src/daemon/pipewire-avb.conf.in
+++ b/src/daemon/pipewire-avb.conf.in
@@ -54,7 +54,7 @@ stream.properties = {
     #node.autoconnect      = true
     #resample.quality      = 4
     #channelmix.normalize  = false
-    #channelmix.mix-lfe    = false
+    #channelmix.mix-lfe    = true
     #channelmix.upmix      = true
     #channelmix.lfe-cutoff = 120
     #channelmix.fc-cutoff  = 6000
diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in
index 2d381a14542007bd6ee6679cec974247e0914318..18bca3a1ec3e77b622685b1bde668c96457c8f3f 100644
--- a/src/daemon/pipewire-pulse.conf.in
+++ b/src/daemon/pipewire-pulse.conf.in
@@ -46,19 +46,30 @@ context.modules = [
     }
 ]
 
-# Extra modules can be loaded here. Setup in default.pa can be moved here
+# Extra scripts can be started here. Setup in default.pa can be moved in
+# a script or in pulse.cmd below
 context.exec = [
-    { path = "pactl"        args = "load-module module-always-sink" }
-    #{ path = "pactl"        args = "load-module module-switch-on-connect" }
+    #{ path = "pactl"        args = "load-module module-always-sink" }
+    #{ path = "pactl"        args = "upload-sample my-sample.wav my-sample" }
     #{ path = "/usr/bin/sh"  args = "~/.config/pipewire/default.pw" }
 ]
 
+# Extra commands can be executed here.
+#   load-module : loads a module with args and flags
+#      args = "<module-name> <module-args>"
+#      flags = [ "no-fail" ]
+pulse.cmd = [
+    { cmd = "load-module" args = "module-always-sink" flags = [ ] }
+    #{ cmd = "load-module" args = "module-switch-on-connect" }
+    #{ cmd = "load-module" args = "module-gsettings" flags = [ "nofail" ] }
+]
+
 stream.properties = {
     #node.latency          = 1024/48000
     #node.autoconnect      = true
     #resample.quality      = 4
     #channelmix.normalize  = false
-    #channelmix.mix-lfe    = false
+    #channelmix.mix-lfe    = true
     #channelmix.upmix      = true
     #channelmix.upmix-method = psd  # none, simple
     #channelmix.lfe-cutoff = 150
@@ -90,7 +101,7 @@ pulse.properties = {
     #pulse.default.frag     = 96000/48000   # 2 seconds
     #pulse.default.tlength  = 96000/48000   # 2 seconds
     #pulse.min.quantum      = 256/48000     # 5ms
-    #pulse.idle.timeout     = 5             # pause after 5s of underruns
+    #pulse.idle.timeout     = 0             # don't pause after underruns
     #pulse.default.format   = F32
     #pulse.default.position = [ FL FR ]
     # These overrides are only applied when running in a vm.
@@ -137,12 +148,12 @@ pulse.rules = [
     }
     {
         # speech dispatcher asks for too small latency and then underruns.
-        matches = [ { application.name = "~speech-dispatcher*" } ]
+        matches = [ { application.name = "~speech-dispatcher.*" } ]
         actions = {
             update-props = {
-                pulse.min.req          = 1024/48000     # 21ms
-                pulse.min.quantum      = 1024/48000     # 21ms
-                #pulse.idle.timeout    = 0
+                pulse.min.req          = 512/48000      # 10.6ms
+                pulse.min.quantum      = 512/48000      # 10.6ms
+                pulse.idle.timeout     = 5              # pause after 5 seconds of underrun
             }
         }
     }
diff --git a/src/examples/video-dsp-play.c b/src/examples/video-dsp-play.c
index 71f9b01998030de63cc47534c9bd8a0dca51ce3c..6068e2a0f761f3a3825d766ab89a563c13981989 100644
--- a/src/examples/video-dsp-play.c
+++ b/src/examples/video-dsp-play.c
@@ -139,6 +139,8 @@ on_process(void *_data, struct spa_io_position *position)
 
 	/* copy video image in texture */
 	sstride = buf->datas[0].chunk->stride;
+	if (sstride == 0)
+		sstride = buf->datas[0].chunk->size / data->position->video.size.height;
 
 	src = sdata;
 	dst = ddata;
diff --git a/src/examples/video-play-fixate.c b/src/examples/video-play-fixate.c
index 021eb0df57f0f857cbdbe1f8845f24942c1e1c79..7477c5a06d3f439b114784cd4bd59168463b5e97 100644
--- a/src/examples/video-play-fixate.c
+++ b/src/examples/video-play-fixate.c
@@ -257,6 +257,8 @@ on_process(void *_data)
 
 	/* copy video image in texture */
 	sstride = buf->datas[0].chunk->stride;
+	if (sstride == 0)
+		sstride = buf->datas[0].chunk->size / data->size.height;
 	ostride = SPA_MIN(sstride, dstride);
 
 	src = sdata;
diff --git a/src/examples/video-play-pull.c b/src/examples/video-play-pull.c
index 8076779629b4e2c66a56b06f930b04298e570263..fd0e3058940953eba1b48ded86f51bca3bd2875b 100644
--- a/src/examples/video-play-pull.c
+++ b/src/examples/video-play-pull.c
@@ -206,6 +206,8 @@ on_process(void *_data)
 		}
 
 		sstride = buf->datas[0].chunk->stride;
+		if (sstride == 0)
+			sstride = buf->datas[0].chunk->size / data->size.height;
 		ostride = SPA_MIN(sstride, dstride);
 
 		src = sdata;
diff --git a/src/examples/video-play-reneg.c b/src/examples/video-play-reneg.c
index f37b66285e6485a488cf93e912a5584f33f7e79c..26b19cb595e1f1ff440a80de0c295ed6c68c44cf 100644
--- a/src/examples/video-play-reneg.c
+++ b/src/examples/video-play-reneg.c
@@ -136,6 +136,8 @@ on_process(void *_data)
 
 	/* copy video image in texture */
 	sstride = buf->datas[0].chunk->stride;
+	if (sstride == 0)
+		sstride = buf->datas[0].chunk->size / data->size.height;
 	ostride = SPA_MIN(sstride, dstride);
 
 	src = sdata;
diff --git a/src/examples/video-play.c b/src/examples/video-play.c
index 61c3114f798d85f4c7da36b9c339c2fa8b6e96d5..9cbbab6987a9ff8921496346db9df791ae79e7ad 100644
--- a/src/examples/video-play.c
+++ b/src/examples/video-play.c
@@ -204,6 +204,8 @@ on_process(void *_data)
 		}
 
 		sstride = buf->datas[0].chunk->stride;
+		if (sstride == 0)
+			sstride = buf->datas[0].chunk->size / data->size.height;
 		ostride = SPA_MIN(sstride, dstride);
 
 		src = sdata;
diff --git a/src/gst/gstpipewirepool.c b/src/gst/gstpipewirepool.c
index e7b57469d49d15a94348e08f463291e646df68a1..7a298b1db0820af59712e206805be33e98ef57a7 100644
--- a/src/gst/gstpipewirepool.c
+++ b/src/gst/gstpipewirepool.c
@@ -115,6 +115,8 @@ void gst_pipewire_pool_wrap_buffer (GstPipeWirePool *pool, struct pw_buffer *b)
   data->crop = spa_buffer_find_meta_data (b->buffer, SPA_META_VideoCrop, sizeof(*data->crop));
   if (data->crop)
 	  gst_buffer_add_video_crop_meta(buf);
+  data->videotransform =
+    spa_buffer_find_meta_data (b->buffer, SPA_META_VideoTransform, sizeof(*data->videotransform));
 
   gst_mini_object_set_qdata (GST_MINI_OBJECT_CAST (buf),
                              pool_data_quark,
diff --git a/src/gst/gstpipewirepool.h b/src/gst/gstpipewirepool.h
index b7b7ca8a754746a2b05873cbda3d4422736b114c..acf81064f2a3b2a0689c3ab2e8867acc06fb7f53 100644
--- a/src/gst/gstpipewirepool.h
+++ b/src/gst/gstpipewirepool.h
@@ -57,6 +57,7 @@ struct _GstPipeWirePoolData {
   GstBuffer *buf;
   gboolean queued;
   struct spa_meta_region *crop;
+  struct spa_meta_videotransform *videotransform;
 };
 
 struct _GstPipeWirePool {
diff --git a/src/gst/gstpipewiresink.c b/src/gst/gstpipewiresink.c
index 16234f51d8bcafced38841b91f0622c64b23d8b5..c7dbe38c5c19052cc240f91d49ae879da40b6ca9 100644
--- a/src/gst/gstpipewiresink.c
+++ b/src/gst/gstpipewiresink.c
@@ -491,6 +491,7 @@ do_send_buffer (GstPipeWireSink *pwsink, GstBuffer *buffer)
     GstMemory *mem = gst_buffer_peek_memory (buffer, i);
     d->chunk->offset = mem->offset;
     d->chunk->size = mem->size;
+    d->chunk->stride = 0;
   }
 
   if ((res = pw_stream_queue_buffer (pwsink->stream, data->b)) < 0) {
diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c
index 4e8e8bd4e7f10f846027443bbb7aadecc6108907..0b222aa0e1fa4d7b09174bde840f4961db025625 100644
--- a/src/gst/gstpipewiresrc.c
+++ b/src/gst/gstpipewiresrc.c
@@ -514,6 +514,25 @@ on_remove_buffer (void *_data, struct pw_buffer *b)
   }
 }
 
+static const char * const transform_map[] = {
+  [SPA_META_TRANSFORMATION_None] = "rotate-0",
+  [SPA_META_TRANSFORMATION_90] = "rotate-90",
+  [SPA_META_TRANSFORMATION_180] = "rotate-180",
+  [SPA_META_TRANSFORMATION_270] = "rotate-270",
+  [SPA_META_TRANSFORMATION_Flipped] = "flip-rotate-0",
+  [SPA_META_TRANSFORMATION_Flipped90] = "flip-rotate-270",
+  [SPA_META_TRANSFORMATION_Flipped180] = "flip-rotate-180",
+  [SPA_META_TRANSFORMATION_Flipped270] = "flip-rotate-90",
+};
+
+static const char *spa_transform_value_to_gst_image_orientation(uint32_t transform_value)
+{
+  if (transform_value >= SPA_N_ELEMENTS(transform_map))
+    transform_value = SPA_META_TRANSFORMATION_None;
+
+  return transform_map[transform_value];
+}
+
 static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc)
 {
   struct pw_buffer *b;
@@ -521,6 +540,7 @@ static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc)
   GstPipeWirePoolData *data;
   struct spa_meta_header *h;
   struct spa_meta_region *crop;
+  struct spa_meta_videotransform *videotransform;
   guint i;
 
   b = pw_stream_dequeue_buffer (pwsrc->stream);
@@ -568,6 +588,27 @@ static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc)
       meta->height = crop->region.size.height;
     }
   }
+
+  videotransform = data->videotransform;
+  if (videotransform) {
+    if (pwsrc->transform_value != videotransform->transform) {
+      GstEvent *tag_event;
+      const char* tag_string;
+
+      tag_string =
+          spa_transform_value_to_gst_image_orientation(videotransform->transform);
+
+      GST_LOG_OBJECT (pwsrc, "got new videotransform: %u / %s",
+          videotransform->transform, tag_string);
+
+      tag_event = gst_event_new_tag(gst_tag_list_new(GST_TAG_IMAGE_ORIENTATION,
+          tag_string, NULL));
+      gst_pad_push_event (GST_BASE_SRC_PAD (pwsrc), tag_event);
+
+      pwsrc->transform_value = videotransform->transform;
+    }
+  }
+
   for (i = 0; i < b->buffer->n_datas; i++) {
     struct spa_data *d = &b->buffer->datas[i];
     GstMemory *pmem = gst_buffer_peek_memory (data->buf, i);
@@ -913,7 +954,7 @@ on_param_changed (void *data, uint32_t id,
   pwsrc->negotiated = pwsrc->caps != NULL;
 
   if (pwsrc->negotiated) {
-    const struct spa_pod *params[3];
+    const struct spa_pod *params[4];
     struct spa_pod_builder b = { NULL };
     uint8_t buffer[512];
     uint32_t buffers = CLAMP (16, pwsrc->min_buffers, pwsrc->max_buffers);
@@ -939,9 +980,13 @@ on_param_changed (void *data, uint32_t id,
         SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
         SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop),
         SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_region)));
+    params[3] = spa_pod_builder_add_object (&b,
+        SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+        SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoTransform),
+        SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_videotransform)));
 
     GST_DEBUG_OBJECT (pwsrc, "doing finish format");
-    pw_stream_update_params (pwsrc->stream, params, 3);
+    pw_stream_update_params (pwsrc->stream, params, SPA_N_ELEMENTS(params));
   } else {
     GST_WARNING_OBJECT (pwsrc, "finish format with error");
     pw_stream_set_error (pwsrc->stream, -EINVAL, "unhandled format");
diff --git a/src/gst/gstpipewiresrc.h b/src/gst/gstpipewiresrc.h
index 128cf39438f58dfc95dd3dcdb89dff015abeadb0..e1def85b3ce072305c38e56ca139e99575a7a9e2 100644
--- a/src/gst/gstpipewiresrc.h
+++ b/src/gst/gstpipewiresrc.h
@@ -95,6 +95,8 @@ struct _GstPipeWireSrc {
   GstPipeWirePool *pool;
   GstClock *clock;
   GstClockTime last_time;
+
+  enum spa_meta_videotransform_value transform_value;
 };
 
 struct _GstPipeWireSrcClass {
diff --git a/src/modules/meson.build b/src/modules/meson.build
index c267e681cd0437852b09b9c56c11bbb57a80501e..367031e98a96980ea106bbdd9fda12866a45933f 100644
--- a/src/modules/meson.build
+++ b/src/modules/meson.build
@@ -100,7 +100,7 @@ filter_chain_sources = [
   'module-filter-chain/convolver.c'
 ]
 filter_chain_dependencies = [
-  mathlib, dl_lib, pipewire_dep, sndfile_dep
+  mathlib, dl_lib, pipewire_dep, sndfile_dep, audioconvert_dep
 ]
 
 if lilv_lib.found()
@@ -226,6 +226,7 @@ pipewire_module_protocol_pulse_sources = [
   'module-protocol-pulse.c',
   'module-protocol-pulse/client.c',
   'module-protocol-pulse/collect.c',
+  'module-protocol-pulse/cmd.c',
   'module-protocol-pulse/extension.c',
   'module-protocol-pulse/extensions/ext-device-manager.c',
   'module-protocol-pulse/extensions/ext-device-restore.c',
@@ -289,6 +290,14 @@ if avahi_dep.found()
   cdata.set('HAVE_AVAHI', true)
 endif
 
+if gio_dep.found()
+  pipewire_module_protocol_pulse_sources += [
+    'module-protocol-pulse/modules/module-gsettings.c',
+  ]
+  pipewire_module_protocol_pulse_deps += gio_dep
+  cdata.set('HAVE_GIO', true)
+endif
+
 if flatpak_support
   pipewire_module_protocol_pulse_deps += glib2_dep
 endif
diff --git a/src/modules/module-client-node/remote-node.c b/src/modules/module-client-node/remote-node.c
index 387711515ec4c16f00df59e92f01661b79b76327..051ab0716205f11918a5bc1dd3d16e24f43f14dc 100644
--- a/src/modules/module-client-node/remote-node.c
+++ b/src/modules/module-client-node/remote-node.c
@@ -1186,7 +1186,7 @@ static int node_ready(void *d, int status)
 	struct timespec ts;
 	struct pw_impl_port *p;
 
-	pw_log_trace("node %p: ready driver:%d exported:%d status:%d", node,
+	pw_log_trace_fp("node %p: ready driver:%d exported:%d status:%d", node,
 			node->driver, node->exported, status);
 
 	if (status & SPA_STATUS_HAVE_DATA) {
diff --git a/src/modules/module-echo-cancel.c b/src/modules/module-echo-cancel.c
index 7e0d81a79fe9f2c9fa8136032006a5b735f38aa4..9dba8c51b083a7209be6b94b4786fe92b91559e2 100644
--- a/src/modules/module-echo-cancel.c
+++ b/src/modules/module-echo-cancel.c
@@ -936,8 +936,13 @@ static void core_error(void *data, uint32_t id, int seq, int res, const char *me
 {
 	struct impl *impl = data;
 
-	pw_log_error("error id:%u seq:%d res:%d (%s): %s",
-			id, seq, res, spa_strerror(res), message);
+	if (res == -ENOENT) {
+		pw_log_info("id:%u seq:%d res:%d (%s): %s",
+				id, seq, res, spa_strerror(res), message);
+	} else {
+		pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+				id, seq, res, spa_strerror(res), message);
+	}
 
 	if (id == PW_ID_CORE && res == -EPIPE)
 		pw_impl_module_schedule_destroy(impl->module);
diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c
index a8f89e394caa8af107baae178a9bb5a91bb00baf..bee9e3e28661ab6d6872dea7c75fbd6ebcf96150 100644
--- a/src/modules/module-filter-chain.c
+++ b/src/modules/module-filter-chain.c
@@ -505,6 +505,7 @@ struct node {
 
 	unsigned int n_deps;
 	unsigned int visited:1;
+	unsigned int disabled:1;
 };
 
 struct link {
@@ -521,6 +522,7 @@ struct graph_port {
 	const struct fc_descriptor *desc;
 	void **hndl;
 	uint32_t port;
+	unsigned next:1;
 };
 
 struct graph_hndl {
@@ -599,7 +601,7 @@ static void playback_process(void *d)
 	struct impl *impl = d;
 	struct pw_buffer *in, *out;
 	struct graph *graph = &impl->graph;
-	uint32_t i, insize = 0, outsize = 0, n_hndl = graph->n_hndl;
+	uint32_t i, j, insize = 0, outsize = 0, n_hndl = graph->n_hndl;
 	int32_t stride = 0;
 	struct graph_port *port;
 	struct spa_data *bd;
@@ -613,7 +615,7 @@ static void playback_process(void *d)
 	if (in == NULL || out == NULL)
 		goto done;
 
-	for (i = 0; i < in->buffer->n_datas; i++) {
+	for (i = 0, j = 0; i < in->buffer->n_datas; i++) {
 		uint32_t offs, size;
 
 		bd = &in->buffer->datas[i];
@@ -621,12 +623,15 @@ static void playback_process(void *d)
 		offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
 		size = SPA_MIN(bd->chunk->size, bd->maxsize - offs);
 
-		port = i < graph->n_input ? &graph->input[i] : NULL;
-
-		if (port && port->desc)
-			port->desc->connect_port(*port->hndl, port->port,
-				SPA_PTROFF(bd->data, offs, void));
+		while (j < graph->n_input) {
+			port = &graph->input[j++];
+			if (port->desc)
+				port->desc->connect_port(*port->hndl, port->port,
+					SPA_PTROFF(bd->data, offs, void));
+			if (!port->next)
+				break;
 
+		}
 		insize = i == 0 ? size : SPA_MIN(insize, size);
 		stride = SPA_MAX(stride, bd->chunk->stride);
 	}
@@ -1849,7 +1854,7 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 		n_nodes++;
 	}
 	graph->n_input = 0;
-	graph->input = calloc(n_input * n_hndl, sizeof(struct graph_port));
+	graph->input = calloc(n_input * 16 * n_hndl, sizeof(struct graph_port));
 	graph->n_output = 0;
 	graph->output = calloc(n_output * n_hndl, sizeof(struct graph_port));
 
@@ -1869,8 +1874,8 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 		} else {
 			struct spa_json it = *inputs;
 			while (spa_json_get_string(&it, v, sizeof(v)) > 0) {
-				gp = &graph->input[graph->n_input];
 				if (spa_streq(v, "null")) {
+					gp = &graph->input[graph->n_input++];
 					gp->desc = NULL;
 					pw_log_info("ignore input port %d", graph->n_input);
 				} else if ((port = find_port(first, v, FC_PORT_INPUT)) == NULL) {
@@ -1893,14 +1898,41 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 						res = -EBUSY;
 						goto error;
 					}
-					pw_log_info("input port %s[%d]:%s",
+
+					if (d->flags & FC_DESCRIPTOR_COPY) {
+						for (j = 0; j < desc->n_output; j++) {
+							struct port *p = &port->node->output_port[j];
+							struct link *link;
+
+							gp = NULL;
+							spa_list_for_each(link, &p->link_list, output_link) {
+								struct port *peer = link->input;
+
+								pw_log_info("copy input port %s[%d]:%s",
+									port->node->name, i,
+									d->ports[port->p].name);
+								peer->external = graph->n_input;
+								gp = &graph->input[graph->n_input++];
+								gp->desc = peer->node->desc->desc;
+								gp->hndl = &peer->node->hndl[i];
+								gp->port = peer->p;
+								gp->next = true;
+							}
+							if (gp != NULL)
+								gp->next = false;
+						}
+						port->node->disabled = true;
+					} else {
+						pw_log_info("input port %s[%d]:%s",
 							port->node->name, i, d->ports[port->p].name);
-					port->external = graph->n_input;
-					gp->desc = d;
-					gp->hndl = &port->node->hndl[i];
-					gp->port = port->p;
+						port->external = graph->n_input;
+						gp = &graph->input[graph->n_input++];
+						gp->desc = d;
+						gp->hndl = &port->node->hndl[i];
+						gp->port = port->p;
+						gp->next = false;
+					}
 				}
-				graph->n_input++;
 			}
 		}
 		if (outputs == NULL) {
@@ -1965,11 +1997,12 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 		desc = node->desc;
 		d = desc->desc;
 
-		for (i = 0; i < n_hndl; i++) {
-			gh = &graph->hndl[graph->n_hndl++];
-			gh->hndl = &node->hndl[i];
-			gh->desc = d;
-
+		if (!node->disabled) {
+			for (i = 0; i < n_hndl; i++) {
+				gh = &graph->hndl[graph->n_hndl++];
+				gh->hndl = &node->hndl[i];
+				gh->desc = d;
+			}
 		}
 		for (i = 0; i < desc->n_output; i++) {
 			spa_list_for_each(link, &node->output_port[i].link_list, output_link)
@@ -2088,8 +2121,13 @@ static void core_error(void *data, uint32_t id, int seq, int res, const char *me
 {
 	struct impl *impl = data;
 
-	pw_log_error("error id:%u seq:%d res:%d (%s): %s",
-			id, seq, res, spa_strerror(res), message);
+	if (res == -ENOENT) {
+		pw_log_info("message id:%u seq:%d res:%d (%s): %s",
+				id, seq, res, spa_strerror(res), message);
+	} else {
+		pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+				id, seq, res, spa_strerror(res), message);
+	}
 
 	if (id == PW_ID_CORE && res == -EPIPE)
 		pw_impl_module_schedule_destroy(impl->module);
diff --git a/src/modules/module-filter-chain/builtin_plugin.c b/src/modules/module-filter-chain/builtin_plugin.c
index 6918ad91d08f14a23da2f06a26b7da4f8786111c..49163686e44cfd8a2ee07e435bc67245f1a72d8d 100644
--- a/src/modules/module-filter-chain/builtin_plugin.c
+++ b/src/modules/module-filter-chain/builtin_plugin.c
@@ -31,7 +31,9 @@
 #endif
 
 #include <spa/utils/json.h>
+#include <spa/utils/result.h>
 #include <spa/support/cpu.h>
+#include <spa/plugins/audioconvert/resample.h>
 
 #include <pipewire/log.h>
 
@@ -101,6 +103,7 @@ static struct fc_port copy_ports[] = {
 
 static const struct fc_descriptor copy_desc = {
 	.name = "copy",
+	.flags = FC_DESCRIPTOR_COPY,
 
 	.n_ports = 2,
 	.ports = copy_ports,
@@ -260,14 +263,11 @@ static struct fc_port bq_ports[] = {
 static void bq_run(struct builtin *impl, unsigned long samples, int type)
 {
 	struct biquad *bq = &impl->bq;
-	unsigned long i;
 	float *out = impl->port[0];
 	float *in = impl->port[1];
 	float freq = impl->port[2][0];
 	float Q = impl->port[3][0];
 	float gain = impl->port[4][0];
-	float x1, x2, y1, y2;
-	float b0, b1, b2, a1, a2;
 
 	if (impl->freq != freq || impl->Q != Q || impl->gain != gain) {
 		impl->freq = freq;
@@ -275,30 +275,7 @@ static void bq_run(struct builtin *impl, unsigned long samples, int type)
 		impl->gain = gain;
 		biquad_set(bq, type, freq * 2 / impl->rate, Q, gain);
 	}
-	x1 = bq->x1;
-	x2 = bq->x2;
-	y1 = bq->y1;
-	y2 = bq->y2;
-	b0 = bq->b0;
-	b1 = bq->b1;
-	b2 = bq->b2;
-	a1 = bq->a1;
-	a2 = bq->a2;
-	for (i = 0; i < samples; i++) {
-		float x = in[i];
-		float y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
-		out[i] = y;
-		x2 = x1;
-		x1 = x;
-		y2 = y1;
-		y1 = y;
-	}
-#define F(x) (-FLT_MIN < (x) && (x) < FLT_MIN ? 0.0f : (x))
-	bq->x1 = F(x1);
-	bq->x2 = F(x2);
-	bq->y1 = F(y1);
-	bq->y2 = F(y2);
-#undef F
+	dsp_ops_biquad_run(&dsp_ops, bq, out, in, samples);
 }
 
 /** bq_lowpass */
@@ -564,6 +541,71 @@ static float *create_dirac(const char *filename, float gain, int delay, int offs
 	return samples;
 }
 
+static float *resample_buffer(float *samples, int *n_samples,
+		unsigned long in_rate, unsigned long out_rate, uint32_t quality)
+{
+	uint32_t in_len, out_len, total_out = 0;
+	int out_n_samples;
+	float *out_samples, *out_buf, *in_buf;
+	struct resample r;
+	int res;
+
+	spa_zero(r);
+	r.channels = 1;
+	r.i_rate = in_rate;
+	r.o_rate = out_rate;
+	r.cpu_flags = dsp_ops.cpu_flags;
+	r.quality = quality;
+	if ((res = resample_native_init(&r)) < 0) {
+		pw_log_error("resampling failed: %s", spa_strerror(res));
+		errno = -res;
+		return NULL;
+	}
+
+	out_n_samples = SPA_ROUND_UP(*n_samples * out_rate, in_rate) / in_rate;
+	out_samples = calloc(out_n_samples, sizeof(float));
+	if (out_samples == NULL)
+		goto error;
+
+	in_len = *n_samples;
+	in_buf = samples;
+	out_len = out_n_samples;
+	out_buf = out_samples;
+
+	pw_log_info("Resampling filter: rate: %lu => %lu, n_samples: %u => %u, q:%u",
+		    in_rate, out_rate, in_len, out_len, quality);
+
+	resample_process(&r, (void*)&in_buf, &in_len, (void*)&out_buf, &out_len);
+	pw_log_debug("resampled: %u -> %u samples", in_len, out_len);
+	total_out += out_len;
+
+	in_len = resample_delay(&r);
+	in_buf = calloc(in_len, sizeof(float));
+	if (in_buf == NULL)
+		goto error;
+
+	out_buf = out_samples + total_out;
+	out_len = out_n_samples - total_out;
+
+	pw_log_debug("flushing resampler: %u in %u out", in_len, out_len);
+	resample_process(&r, (void*)&in_buf, &in_len, (void*)&out_buf, &out_len);
+	pw_log_debug("flushed: %u -> %u samples", in_len, out_len);
+	total_out += out_len;
+
+	free(in_buf);
+	free(samples);
+	resample_free(&r);
+
+	*n_samples = total_out;
+	return out_samples;
+
+error:
+	resample_free(&r);
+	free(samples);
+	free(out_samples);
+	return NULL;
+}
+
 static void * convolver_instantiate(const struct fc_descriptor * Descriptor,
 		unsigned long SampleRate, int index, const char *config)
 {
@@ -576,6 +618,7 @@ static void * convolver_instantiate(const struct fc_descriptor * Descriptor,
 	char filename[PATH_MAX] = "";
 	int blocksize = 0, tailsize = 0;
 	int delay = 0;
+	int resample_quality = RESAMPLE_DEFAULT_QUALITY;
 	float gain = 1.0f;
 	unsigned long rate;
 
@@ -636,6 +679,12 @@ static void * convolver_instantiate(const struct fc_descriptor * Descriptor,
 				return NULL;
 			}
 		}
+		else if (spa_streq(key, "resample_quality")) {
+			if (spa_json_get_int(&it[1], &resample_quality) <= 0) {
+				pw_log_error("convolver:resample_quality requires a number");
+				return NULL;
+			}
+		}
 		else if (spa_json_next(&it[1], &val) < 0)
 			break;
 	}
@@ -659,10 +708,9 @@ static void * convolver_instantiate(const struct fc_descriptor * Descriptor,
 		rate = SampleRate;
 		samples = read_samples(filename, gain, delay, offset,
 				length, channel, &rate, &n_samples);
-		if (rate != SampleRate) {
-			pw_log_warn("Convolver samplerate %lu doesn't match filter rate %lu. "
-					"Consider forcing a filter rate.", rate, SampleRate);
-		}
+		if (rate != SampleRate)
+			samples = resample_buffer(samples, &n_samples,
+					rate, SampleRate, resample_quality);
 	}
 	if (samples == NULL) {
 		errno = ENOENT;
diff --git a/src/modules/module-filter-chain/convolver.c b/src/modules/module-filter-chain/convolver.c
index fff4fbe179098ea755099514f42c302de31ace6c..f834e7a10112d03a595ce408c66b49a63fe24eec 100644
--- a/src/modules/module-filter-chain/convolver.c
+++ b/src/modules/module-filter-chain/convolver.c
@@ -420,8 +420,6 @@ void convolver_free(struct convolver *conv)
 
 int convolver_run(struct convolver *conv, const float *input, float *output, int length)
 {
-	int i;
-
 	convolver1_run(conv->headConvolver, input, output, length);
 
 	if (conv->tailInput) {
@@ -431,24 +429,14 @@ int convolver_run(struct convolver *conv, const float *input, float *output, int
 			int remaining = length - processed;
 			int processing = SPA_MIN(remaining, conv->headBlockSize - (conv->tailInputFill % conv->headBlockSize));
 
-			const int sumBegin = processed;
-			const int sumEnd = processed + processing;
-
-			if (conv->tailPrecalculated0) {
-				int precalculatedPos = conv->precalculatedPos;
-				for (i = sumBegin; i < sumEnd; i++) {
-					output[i] += conv->tailPrecalculated0[precalculatedPos];
-					precalculatedPos++;
-				}
-			}
-
-			if (conv->tailPrecalculated) {
-				int precalculatedPos = conv->precalculatedPos;
-				for (i = sumBegin; i < sumEnd; i++) {
-					output[i] += conv->tailPrecalculated[precalculatedPos];
-					precalculatedPos++;
-				}
-			}
+			if (conv->tailPrecalculated0)
+				fft_sum(&output[processed], &output[processed],
+						&conv->tailPrecalculated0[conv->precalculatedPos],
+						processing);
+			if (conv->tailPrecalculated)
+				fft_sum(&output[processed], &output[processed],
+						&conv->tailPrecalculated[conv->precalculatedPos],
+						processing);
 			conv->precalculatedPos += processing;
 
 			fft_copy(conv->tailInput + conv->tailInputFill, input + processed, processing);
@@ -467,7 +455,8 @@ int convolver_run(struct convolver *conv, const float *input, float *output, int
 			if (conv->tailPrecalculated &&
 			    conv->tailInputFill == conv->tailBlockSize) {
 				SPA_SWAP(conv->tailPrecalculated, conv->tailOutput);
-				convolver1_run(conv->tailConvolver, conv->tailInput, conv->tailOutput, conv->tailBlockSize);
+				convolver1_run(conv->tailConvolver, conv->tailInput,
+						conv->tailOutput, conv->tailBlockSize);
 			}
 			if (conv->tailInputFill == conv->tailBlockSize) {
 				conv->tailInputFill = 0;
diff --git a/src/modules/module-filter-chain/dsp-ops-c.c b/src/modules/module-filter-chain/dsp-ops-c.c
index 885c8cd38135995d91cb31eed940c6436263e689..d559a415b88356e88bd4acdc3309ca832633eb45 100644
--- a/src/modules/module-filter-chain/dsp-ops-c.c
+++ b/src/modules/module-filter-chain/dsp-ops-c.c
@@ -25,6 +25,7 @@
 #include <string.h>
 #include <stdio.h>
 #include <math.h>
+#include <float.h>
 
 #include <spa/utils/defs.h>
 
@@ -98,3 +99,37 @@ void dsp_mix_gain_c(struct dsp_ops *ops,
 		}
 	}
 }
+
+void dsp_biquad_run_c(struct dsp_ops *ops, struct biquad *bq,
+		float *out, const float *in, uint32_t n_samples)
+{
+	float x1, x2, y1, y2;
+	float b0, b1, b2, a1, a2;
+	uint32_t i;
+
+	x1 = bq->x1;
+	x2 = bq->x2;
+	y1 = bq->y1;
+	y2 = bq->y2;
+	b0 = bq->b0;
+	b1 = bq->b1;
+	b2 = bq->b2;
+	a1 = bq->a1;
+	a2 = bq->a2;
+	for (i = 0; i < n_samples; i++) {
+		float x = in[i];
+		float y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
+		out[i] = y;
+		x2 = x1;
+		x1 = x;
+		y2 = y1;
+		y1 = y;
+	}
+#define F(x) (-FLT_MIN < (x) && (x) < FLT_MIN ? 0.0f : (x))
+	bq->x1 = F(x1);
+	bq->x2 = F(x2);
+	bq->y1 = F(y1);
+	bq->y2 = F(y2);
+#undef F
+}
+
diff --git a/src/modules/module-filter-chain/dsp-ops.c b/src/modules/module-filter-chain/dsp-ops.c
index b40278bf644d27919aed280914407df6ec6c4bff..2fbf5c32fe067c0d8eff4e24c5f7d7bc02133137 100644
--- a/src/modules/module-filter-chain/dsp-ops.c
+++ b/src/modules/module-filter-chain/dsp-ops.c
@@ -42,6 +42,8 @@ struct dsp_info {
 			void * SPA_RESTRICT dst,
 			const void * SPA_RESTRICT src[],
 			float gain[], uint32_t n_src, uint32_t n_samples);
+	void (*biquad_run) (struct dsp_ops *ops, struct biquad *bq,
+			float *out, const float *in, uint32_t n_samples);
 };
 
 static struct dsp_info dsp_table[] =
@@ -50,11 +52,13 @@ static struct dsp_info dsp_table[] =
 	{ SPA_CPU_FLAG_SSE,
 		.copy = dsp_copy_c,
 		.mix_gain = dsp_mix_gain_sse,
+		.biquad_run = dsp_biquad_run_c,
 	},
 #endif
 	{ 0,
 		.copy = dsp_copy_c,
 		.mix_gain = dsp_mix_gain_c,
+		.biquad_run = dsp_biquad_run_c,
 	},
 };
 
@@ -86,6 +90,7 @@ int dsp_ops_init(struct dsp_ops *ops)
 	ops->cpu_flags = info->cpu_flags;
 	ops->copy = info->copy;
 	ops->mix_gain = info->mix_gain;
+	ops->biquad_run = info->biquad_run;
 	ops->free = impl_dsp_ops_free;
 
 	return 0;
diff --git a/src/modules/module-filter-chain/dsp-ops.h b/src/modules/module-filter-chain/dsp-ops.h
index ffbca6ce7a4d549b1c9d40652a695d8a0fc946c5..3bf7cda19080993489473e275c812ea6b8c1efd5 100644
--- a/src/modules/module-filter-chain/dsp-ops.h
+++ b/src/modules/module-filter-chain/dsp-ops.h
@@ -24,6 +24,8 @@
 
 #include <spa/utils/defs.h>
 
+#include "biquad.h"
+
 struct dsp_ops {
 	uint32_t cpu_flags;
 
@@ -35,6 +37,8 @@ struct dsp_ops {
 			void * SPA_RESTRICT dst,
 			const void * SPA_RESTRICT src[],
 			float gain[], uint32_t n_src, uint32_t n_samples);
+	void (*biquad_run) (struct dsp_ops *ops, struct biquad *bq,
+			float *out, const float *in, uint32_t n_samples);
 	void (*free) (struct dsp_ops *ops);
 
 	const void *priv;
@@ -44,19 +48,24 @@ int dsp_ops_init(struct dsp_ops *ops);
 
 #define dsp_ops_copy(ops,...)		(ops)->copy(ops, __VA_ARGS__)
 #define dsp_ops_mix_gain(ops,...)	(ops)->mix_gain(ops, __VA_ARGS__)
+#define dsp_ops_biquad_run(ops,...)	(ops)->biquad_run(ops, __VA_ARGS__)
 #define dsp_ops_free(ops)		(ops)->free(ops)
 
 
 #define MAKE_COPY_FUNC(arch) \
 void dsp_copy_##arch(struct dsp_ops *ops, void * SPA_RESTRICT dst, \
-			const void * SPA_RESTRICT src, uint32_t n_samples)
+	const void * SPA_RESTRICT src, uint32_t n_samples)
 #define MAKE_MIX_GAIN_FUNC(arch) \
 void dsp_mix_gain_##arch(struct dsp_ops *ops, void * SPA_RESTRICT dst,	\
 	const void * SPA_RESTRICT src[], float gain[], uint32_t n_src, uint32_t n_samples)
+#define MAKE_BIQUAD_RUN_FUNC(arch) \
+void dsp_biquad_run_##arch (struct dsp_ops *ops, struct biquad *bq,	\
+	float *out, const float *in, uint32_t n_samples)
 
 
 MAKE_COPY_FUNC(c);
 MAKE_MIX_GAIN_FUNC(c);
+MAKE_BIQUAD_RUN_FUNC(c);
 #if defined (HAVE_SSE)
 MAKE_MIX_GAIN_FUNC(sse);
 #endif
diff --git a/src/modules/module-filter-chain/plugin.h b/src/modules/module-filter-chain/plugin.h
index ce46b62ecf87bdaf2f442ad499728f2b37fb0b09..12ead2d97ca88090de8b1e8f0fb1094682b860a5 100644
--- a/src/modules/module-filter-chain/plugin.h
+++ b/src/modules/module-filter-chain/plugin.h
@@ -64,6 +64,7 @@ struct fc_port {
 struct fc_descriptor {
 	const char *name;
 #define FC_DESCRIPTOR_SUPPORTS_NULL_DATA	(1ULL << 0)
+#define FC_DESCRIPTOR_COPY			(1ULL << 1)
 	uint64_t flags;
 
 	void (*free) (const struct fc_descriptor *desc);
diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c
index 505ff1c1381a0352ed375feb968c4dbad69e8b4e..0c61e86e431dd84e4860c80b6cf90d80fb4c522a 100644
--- a/src/modules/module-loopback.c
+++ b/src/modules/module-loopback.c
@@ -451,32 +451,31 @@ static int setup_streams(struct impl *impl)
 			&impl->playback_listener,
 			&out_stream_events, impl);
 
+	/* connect playback first to activate it before capture triggers it */
 	n_params = 0;
 	spa_pod_builder_init(&b, buffer, sizeof(buffer));
 	params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
-			&impl->capture_info);
-
-	if ((res = pw_stream_connect(impl->capture,
-			PW_DIRECTION_INPUT,
+			&impl->playback_info);
+	if ((res = pw_stream_connect(impl->playback,
+			PW_DIRECTION_OUTPUT,
 			PW_ID_ANY,
 			PW_STREAM_FLAG_AUTOCONNECT |
 			PW_STREAM_FLAG_MAP_BUFFERS |
-			PW_STREAM_FLAG_RT_PROCESS,
+			PW_STREAM_FLAG_RT_PROCESS |
+			PW_STREAM_FLAG_TRIGGER,
 			params, n_params)) < 0)
 		return res;
 
 	n_params = 0;
 	spa_pod_builder_init(&b, buffer, sizeof(buffer));
 	params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
-			&impl->playback_info);
-
-	if ((res = pw_stream_connect(impl->playback,
-			PW_DIRECTION_OUTPUT,
+			&impl->capture_info);
+	if ((res = pw_stream_connect(impl->capture,
+			PW_DIRECTION_INPUT,
 			PW_ID_ANY,
 			PW_STREAM_FLAG_AUTOCONNECT |
 			PW_STREAM_FLAG_MAP_BUFFERS |
-			PW_STREAM_FLAG_RT_PROCESS |
-			PW_STREAM_FLAG_TRIGGER,
+			PW_STREAM_FLAG_RT_PROCESS,
 			params, n_params)) < 0)
 		return res;
 
@@ -487,8 +486,13 @@ static void core_error(void *data, uint32_t id, int seq, int res, const char *me
 {
 	struct impl *impl = data;
 
-	pw_log_error("error id:%u seq:%d res:%d (%s): %s",
-			id, seq, res, spa_strerror(res), message);
+	if (res == -ENOENT) {
+		pw_log_info("message id:%u seq:%d res:%d (%s): %s",
+				id, seq, res, spa_strerror(res), message);
+	} else {
+		pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+				id, seq, res, spa_strerror(res), message);
+	}
 
 	if (id == PW_ID_CORE && res == -EPIPE)
 		pw_impl_module_schedule_destroy(impl->module);
@@ -513,11 +517,11 @@ static const struct pw_proxy_events core_proxy_events = {
 
 static void impl_destroy(struct impl *impl)
 {
-	/* disconnect both streams before destroying any of them */
+	/* deactivate both streams before destroying any of them */
 	if (impl->capture)
-		pw_stream_disconnect(impl->capture);
+		pw_stream_set_active(impl->capture, false);
 	if (impl->playback)
-		pw_stream_disconnect(impl->playback);
+		pw_stream_set_active(impl->playback, false);
 
 	if (impl->capture)
 		pw_stream_destroy(impl->capture);
diff --git a/src/modules/module-protocol-pulse.c b/src/modules/module-protocol-pulse.c
index 29ba0c09241fc0c3f1b322abaec150cdbc00b0ae..eaf4c2f0e0ae6561ded2e4b4632ed5f8a83613f0 100644
--- a/src/modules/module-protocol-pulse.c
+++ b/src/modules/module-protocol-pulse.c
@@ -305,6 +305,7 @@
 
 PW_LOG_TOPIC(mod_topic, "mod." NAME);
 #define PW_LOG_TOPIC_DEFAULT mod_topic
+PW_LOG_TOPIC(pulse_conn, "conn." NAME);
 PW_LOG_TOPIC(pulse_ext_dev_restore, "mod." NAME ".device-restore");
 PW_LOG_TOPIC(pulse_ext_stream_restore, "mod." NAME ".stream-restore");
 
@@ -354,6 +355,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	int res;
 
 	PW_LOG_TOPIC_INIT(mod_topic);
+	PW_LOG_TOPIC_INIT(pulse_conn);
 	/* it's easier to init these here than adding an init() call to the
 	 * extensions */
 	PW_LOG_TOPIC_INIT(pulse_ext_dev_restore);
diff --git a/src/modules/module-protocol-pulse/cmd.c b/src/modules/module-protocol-pulse/cmd.c
new file mode 100644
index 0000000000000000000000000000000000000000..e8e6406caaf674264ef8aa8501f6e66900db81ee
--- /dev/null
+++ b/src/modules/module-protocol-pulse/cmd.c
@@ -0,0 +1,136 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+
+#include <pipewire/utils.h>
+
+#include "module.h"
+#include "cmd.h"
+
+static const char WHITESPACE[] = " \t\n\r";
+
+static int do_load_module(struct impl *impl, char *args, const char *flags)
+{
+	int res, n;
+	struct module *module;
+	char *a[2] = { NULL };
+
+	n = pw_split_ip(args, WHITESPACE, 2, a);
+	if (n < 1) {
+		pw_log_info("load-module expects module name");
+		return -EINVAL;
+	}
+
+	module = module_create(impl, a[0], a[1]);
+	if (module == NULL)
+		return -errno;
+	if ((res = module_load(module)) < 0)
+		return res;
+
+	return res;
+}
+
+static int do_cmd(struct impl *impl, const char *cmd, char *args, const char *flags)
+{
+	int res = 0;
+	if (spa_streq(cmd, "load-module")) {
+		res = do_load_module(impl, args, flags);
+	} else {
+		pw_log_warn("ignoring unknown command `%s` with args `%s`",
+				cmd, args);
+	}
+	if (res < 0) {
+		if (flags && strstr(flags, "nofail")) {
+			pw_log_info("nofail command %s %s: %s",
+					cmd, args, spa_strerror(res));
+			res = 0;
+		} else {
+			pw_log_error("can't run command %s %s: %s",
+					cmd, args, spa_strerror(res));
+		}
+	}
+	return res;
+}
+
+/*
+ * pulse.cmd = [
+ *   { cmd = <command> [ args = "<arguments>" ] }
+ *   ...
+ * ]
+ */
+static int parse_cmd(void *user_data, const char *location,
+		const char *section, const char *str, size_t len)
+{
+	struct impl *impl = user_data;
+	struct spa_json it[3];
+	char key[512], *s;
+	int res = 0;
+
+	s = strndup(str, len);
+	spa_json_init(&it[0], s, len);
+	if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+		pw_log_error("config file error: pulse.cmd is not an array");
+		res = -EINVAL;
+		goto exit;
+	}
+
+	while (spa_json_enter_object(&it[1], &it[2]) > 0) {
+		char *cmd = NULL, *args = NULL, *flags = NULL;
+
+		while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+			const char *val;
+			int len;
+
+			if ((len = spa_json_next(&it[2], &val)) <= 0)
+				break;
+
+			if (spa_streq(key, "cmd")) {
+				cmd = (char*)val;
+				spa_json_parse_stringn(val, len, cmd, len+1);
+			} else if (spa_streq(key, "args")) {
+				args = (char*)val;
+				spa_json_parse_stringn(val, len, args, len+1);
+			} else if (spa_streq(key, "flags")) {
+				if (spa_json_is_container(val, len))
+					len = spa_json_container_len(&it[2], val, len);
+				flags = (char*)val;
+				spa_json_parse_stringn(val, len, flags, len+1);
+			}
+		}
+		if (cmd != NULL)
+			res = do_cmd(impl, cmd, args, flags);
+		if (res < 0)
+			break;
+	}
+exit:
+	free(s);
+	return res;
+}
+
+int cmd_run(struct impl *impl)
+{
+	return pw_context_conf_section_for_each(impl->context, "pulse.cmd",
+				parse_cmd, impl);
+}
diff --git a/src/modules/module-protocol-pulse/cmd.h b/src/modules/module-protocol-pulse/cmd.h
new file mode 100644
index 0000000000000000000000000000000000000000..81f528c3240adeb0424bb45282ce66eef40555bb
--- /dev/null
+++ b/src/modules/module-protocol-pulse/cmd.h
@@ -0,0 +1,32 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_CMD_H
+#define PULSER_SERVER_CMD_H
+
+#include "internal.h"
+
+int cmd_run(struct impl *impl);
+
+#endif /* PULSER_SERVER_CMD_H */
diff --git a/src/modules/module-protocol-pulse/message.c b/src/modules/module-protocol-pulse/message.c
index e70b03a2bcd87233221925ed2a74abc6106aa011..daf4d2e95a5549b17c2d77db41a6e7f4970b9737 100644
--- a/src/modules/module-protocol-pulse/message.c
+++ b/src/modules/module-protocol-pulse/message.c
@@ -33,7 +33,6 @@
 #include "defs.h"
 #include "format.h"
 #include "internal.h"
-#include "log.h"
 #include "message.h"
 #include "remap.h"
 #include "volume.h"
@@ -47,6 +46,9 @@
 
 #define PA_CHANNELS_MAX	(32u)
 
+PW_LOG_TOPIC_EXTERN(pulse_conn);
+#define PW_LOG_TOPIC_DEFAULT pulse_conn
+
 static inline uint32_t volume_from_linear(float vol)
 {
 	uint32_t v;
diff --git a/src/modules/module-protocol-pulse/module.c b/src/modules/module-protocol-pulse/module.c
index 27a5b6b1feb5fdaf25d8121c94295e8163b66866..ec1040411b26bb5abecc0d97d47b97dbe5dfe98f 100644
--- a/src/modules/module-protocol-pulse/module.c
+++ b/src/modules/module-protocol-pulse/module.c
@@ -36,7 +36,6 @@
 #include <pipewire/properties.h>
 #include <pipewire/work-queue.h>
 
-#include "client.h"
 #include "defs.h"
 #include "format.h"
 #include "internal.h"
@@ -84,14 +83,14 @@ void module_add_listener(struct module *module,
 	spa_hook_list_append(&module->listener_list, listener, events, data);
 }
 
-int module_load(struct client *client, struct module *module)
+int module_load(struct module *module)
 {
 	pw_log_info("load module index:%u name:%s", module->index, module->info->name);
 	if (module->info->load == NULL)
 		return -ENOTSUP;
 	/* subscription event is sent when the module does a
 	 * module_emit_loaded() */
-	return module->info->load(client, module);
+	return module->info->load(module);
 }
 
 void module_free(struct module *module)
@@ -119,9 +118,6 @@ int module_unload(struct module *module)
 	struct impl *impl = module->impl;
 	int res = 0;
 
-	/* Note that client can be NULL (when the module is being unloaded
-	 * internally and not by a client request */
-
 	pw_log_info("unload module index:%u name:%s", module->index, module->info->name);
 
 	if (module->info->unload)
@@ -283,9 +279,8 @@ static int find_module_by_name(void *item_data, void *data)
 	return spa_streq(module->info->name, name) ? 1 : 0;
 }
 
-struct module *module_create(struct client *client, const char *name, const char *args)
+struct module *module_create(struct impl *impl, const char *name, const char *args)
 {
-	struct impl *impl = client->impl;
 	const struct module_info *info;
 	struct module *module;
 
diff --git a/src/modules/module-protocol-pulse/module.h b/src/modules/module-protocol-pulse/module.h
index c767d689698fbcba51dfd404f4422877ff504b39..1a6ffb04c89c3a3baa09061ffc91a86567def4a0 100644
--- a/src/modules/module-protocol-pulse/module.h
+++ b/src/modules/module-protocol-pulse/module.h
@@ -29,7 +29,6 @@
 #include <spa/param/audio/raw.h>
 #include <spa/utils/hook.h>
 
-#include "client.h"
 #include "internal.h"
 
 struct module;
@@ -41,7 +40,7 @@ struct module_info {
 	unsigned int load_once:1;
 
 	int (*prepare) (struct module *module);
-	int (*load) (struct client *client, struct module *module);
+	int (*load) (struct module *module);
 	int (*unload) (struct module *module);
 
 	const struct spa_dict *properties;
@@ -78,9 +77,9 @@ struct module {
 #define module_emit_loaded(m,r) spa_hook_list_call(&m->listener_list, struct module_events, loaded, 0, r)
 #define module_emit_destroy(m) spa_hook_list_call(&(m)->listener_list, struct module_events, destroy, 0)
 
-struct module *module_create(struct client *client, const char *name, const char *args);
+struct module *module_create(struct impl *impl, const char *name, const char *args);
 void module_free(struct module *module);
-int module_load(struct client *client, struct module *module);
+int module_load(struct module *module);
 int module_unload(struct module *module);
 void module_schedule_unload(struct module *module);
 
diff --git a/src/modules/module-protocol-pulse/modules/module-always-sink.c b/src/modules/module-protocol-pulse/modules/module-always-sink.c
index dc3d7aaadd5460d304cb418616deb566a8dd61df..7549c09286bd879762b3ede4c42ddb26a566fa14 100644
--- a/src/modules/module-protocol-pulse/modules/module-always-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-always-sink.c
@@ -51,7 +51,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_always_sink_load(struct client *client, struct module *module)
+static int module_always_sink_load(struct module *module)
 {
 	struct module_always_sink_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-combine-sink.c b/src/modules/module-protocol-pulse/modules/module-combine-sink.c
index 5d5ff77e5aaa16e4b4d1b73668c6933dfcdbb5da..93f6be69d38e0eac64c8afdbccf0ed9fa3fed075 100644
--- a/src/modules/module-protocol-pulse/modules/module-combine-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-combine-sink.c
@@ -391,7 +391,7 @@ static void on_sinks_timeout(void *d, uint64_t count)
 	check_initialized(data);
 }
 
-static int module_combine_sink_load(struct client *client, struct module *module)
+static int module_combine_sink_load(struct module *module)
 {
 	struct module_combine_sink_data *data = module->user_data;
 	struct pw_properties *props;
@@ -402,9 +402,7 @@ static int module_combine_sink_load(struct client *client, struct module *module
 	struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
 	const char *str;
 
-	data->core = pw_context_connect(module->impl->context,
-			pw_properties_copy(client->props),
-			0);
+	data->core = pw_context_connect(module->impl->context, NULL, 0);
 	if (data->core == NULL)
 		return -errno;
 
diff --git a/src/modules/module-protocol-pulse/modules/module-echo-cancel.c b/src/modules/module-protocol-pulse/modules/module-echo-cancel.c
index 9ecf19fa577b581234b2cb951104eb72b76b09c0..47dfa75bbd087055391fb231a81385b930865a31 100644
--- a/src/modules/module-protocol-pulse/modules/module-echo-cancel.c
+++ b/src/modules/module-protocol-pulse/modules/module-echo-cancel.c
@@ -63,7 +63,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_echo_cancel_load(struct client *client, struct module *module)
+static int module_echo_cancel_load(struct module *module)
 {
 	struct module_echo_cancel_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-gsettings.c b/src/modules/module-protocol-pulse/modules/module-gsettings.c
new file mode 100644
index 0000000000000000000000000000000000000000..36e216ba7abf034dee2fb57508093aaaf42ca47a
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-gsettings.c
@@ -0,0 +1,298 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <gio/gio.h>
+#include <glib.h>
+
+#include <spa/debug/mem.h>
+#include <pipewire/pipewire.h>
+#include <pipewire/thread.h>
+
+#include "../module.h"
+
+#define NAME "gsettings"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define PA_GSETTINGS_MODULE_GROUP_SCHEMA "org.freedesktop.pulseaudio.module-group"
+#define PA_GSETTINGS_MODULE_GROUPS_SCHEMA "org.freedesktop.pulseaudio.module-groups"
+#define PA_GSETTINGS_MODULE_GROUPS_PATH "/org/freedesktop/pulseaudio/module-groups/"
+
+#define MAX_MODULES	10
+
+struct module_gsettings_data {
+	struct module *module;
+
+	GMainContext *context;
+	GMainLoop *loop;
+	struct spa_thread *thr;
+
+	GSettings *settings;
+	gchar **group_names;
+
+	struct spa_list groups;
+};
+
+struct group {
+	struct spa_list link;
+	char *name;
+	struct module *module;
+	struct spa_hook module_listener;
+};
+
+struct info {
+	bool enabled;
+	char *name;
+	char *module[MAX_MODULES];
+	char *args[MAX_MODULES];
+};
+
+static void clean_info(const struct info *info)
+{
+	int i;
+	for (i = 0; i < MAX_MODULES; i++) {
+		g_free(info->module[i]);
+		g_free(info->args[i]);
+	}
+	g_free(info->name);
+}
+
+static void unload_module(struct module_gsettings_data *d, struct group *g)
+{
+	spa_list_remove(&g->link);
+	g_free(g->name);
+	if (g->module)
+		module_unload(g->module);
+	free(g);
+}
+
+static void unload_group(struct module_gsettings_data *d, const char *name)
+{
+	struct group *g, *t;
+	spa_list_for_each_safe(g, t, &d->groups, link) {
+		if (spa_streq(g->name, name))
+			unload_module(d, g);
+	}
+}
+static void module_destroy(void *data)
+{
+	struct group *g = data;
+	if (g->module) {
+		spa_hook_remove(&g->module_listener);
+		g->module = NULL;
+	}
+}
+
+static const struct module_events module_gsettings_events = {
+	VERSION_MODULE_EVENTS,
+	.destroy = module_destroy
+};
+
+static int load_group(struct module_gsettings_data *d, const struct info *info)
+{
+	struct group *g;
+	int i, res;
+
+	for (i = 0; i < MAX_MODULES; i++) {
+		if (info->module[i] == NULL || strlen(info->module[i]) <= 0)
+			break;
+
+		g = calloc(1, sizeof(struct group));
+		if (g == NULL)
+			return -errno;
+
+		g->name = strdup(info->name);
+		g->module = module_create(d->module->impl, info->module[i], info->args[i]);
+		if (g->module == NULL) {
+			pw_log_info("can't create module:%s args:%s: %m",
+					info->module[i], info->args[i]);
+		} else {
+			module_add_listener(g->module, &g->module_listener,
+					&module_gsettings_events, g);
+			if ((res = module_load(g->module)) < 0) {
+				pw_log_warn("can't load module:%s args:%s: %s",
+						info->module[i], info->args[i],
+						spa_strerror(res));
+			}
+		}
+		spa_list_append(&d->groups, &g->link);
+	}
+	return 0;
+}
+
+static int
+do_handle_info(struct spa_loop *loop,
+		bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+	struct module_gsettings_data *d = user_data;
+	const struct info *info = data;
+
+	unload_group(d, info->name);
+	if (info->enabled)
+		load_group(d, info);
+
+	clean_info(info);
+	return 0;
+}
+
+static void handle_module_group(struct module_gsettings_data *d, gchar *name)
+{
+	struct impl *impl = d->module->impl;
+	GSettings *settings;
+	gchar p[1024];
+	struct info info;
+	int i;
+
+	snprintf(p, sizeof(p), PA_GSETTINGS_MODULE_GROUPS_PATH"%s/", name);
+
+	settings = g_settings_new_with_path(PA_GSETTINGS_MODULE_GROUP_SCHEMA, p);
+	if (settings == NULL)
+		return;
+
+	spa_zero(info);
+	info.name = strdup(p);
+	info.enabled = g_settings_get_boolean(settings, "enabled");
+
+	for (i = 0; i < MAX_MODULES; i++) {
+		snprintf(p, sizeof(p), "name%d", i);
+		info.module[i] = g_settings_get_string(settings, p);
+
+		snprintf(p, sizeof(p), "args%i", i);
+		info.args[i] = g_settings_get_string(settings, p);
+	}
+	pw_loop_invoke(impl->loop, do_handle_info, 0,
+			&info, sizeof(info), false, d);
+
+	g_object_unref(G_OBJECT(settings));
+}
+
+static void module_group_callback(GSettings *settings, gchar *key, gpointer user_data)
+{
+	struct module_gsettings_data *d = g_object_get_data(G_OBJECT(settings), "module-data");
+	handle_module_group(d, user_data);
+}
+
+static void *do_loop(void *user_data)
+{
+	struct module_gsettings_data *d = user_data;
+
+	pw_log_info("enter");
+	g_main_context_push_thread_default(d->context);
+
+	d->loop = g_main_loop_new(d->context, FALSE);
+
+	g_main_loop_run(d->loop);
+
+	g_main_context_pop_thread_default(d->context);
+	g_main_loop_unref (d->loop);
+	d->loop = NULL;
+	pw_log_info("leave");
+
+	return NULL;
+}
+
+static int module_gsettings_load(struct module *module)
+{
+	struct module_gsettings_data *data = module->user_data;
+	gchar **name;
+
+	data->context = g_main_context_new();
+	g_main_context_push_thread_default(data->context);
+
+	data->settings = g_settings_new(PA_GSETTINGS_MODULE_GROUPS_SCHEMA);
+	if (data->settings == NULL)
+		return -EIO;
+
+	data->group_names = g_settings_list_children(data->settings);
+
+	for (name = data->group_names; *name; name++) {
+		GSettings *child = g_settings_get_child(data->settings, *name);
+		/* The child may have been removed between the
+		 * g_settings_list_children() and g_settings_get_child() calls. */
+		if (child == NULL)
+			continue;
+
+		g_object_set_data(G_OBJECT(child), "module-data", data);
+		g_signal_connect(child, "changed", (GCallback) module_group_callback, *name);
+		handle_module_group(data, *name);
+	}
+	g_main_context_pop_thread_default(data->context);
+
+	data->thr = pw_thread_utils_create(NULL, do_loop, data);
+	return 0;
+}
+
+static gboolean
+do_stop(gpointer data)
+{
+	struct module_gsettings_data *d = data;
+	if (d->loop)
+		g_main_loop_quit(d->loop);
+	return FALSE;
+}
+
+static int module_gsettings_unload(struct module *module)
+{
+	struct module_gsettings_data *d = module->user_data;
+	struct group *g;
+
+	g_main_context_invoke(d->context, do_stop, d);
+	pw_thread_utils_join(d->thr, NULL);
+	g_main_context_unref(d->context);
+
+	spa_list_consume(g, &d->groups, link)
+		unload_module(d, g);
+
+	g_strfreev(d->group_names);
+	g_object_unref(G_OBJECT(d->settings));
+	return 0;
+}
+
+static int module_gsettings_prepare(struct module * const module)
+{
+	PW_LOG_TOPIC_INIT(mod_topic);
+
+	struct module_gsettings_data * const data = module->user_data;
+	spa_list_init(&data->groups);
+	data->module = module;
+
+	return 0;
+}
+
+static const struct spa_dict_item module_gsettings_info[] = {
+	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+	{ PW_KEY_MODULE_DESCRIPTION, "GSettings Adapter" },
+	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+DEFINE_MODULE_INFO(module_gsettings) = {
+	.name = "module-gsettings",
+	.load_once = true,
+	.prepare = module_gsettings_prepare,
+	.load = module_gsettings_load,
+	.unload = module_gsettings_unload,
+	.properties = &SPA_DICT_INIT_ARRAY(module_gsettings_info),
+	.data_size = sizeof(struct module_gsettings_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c b/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c
index 33532fa8a0b50fa84efa7f3c24f156e0433d87fd..051926ffab270e86ec1f4adb8bd1663008ab4c0e 100644
--- a/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c
@@ -59,7 +59,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_ladspa_sink_load(struct client *client, struct module *module)
+static int module_ladspa_sink_load(struct module *module)
 {
 	struct module_ladspa_sink_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-ladspa-source.c b/src/modules/module-protocol-pulse/modules/module-ladspa-source.c
index 246f40f9d5a3db4fa4cf61e3e037a464a9cb45e9..4d65a26c0a11603230f1500997f58e5a8b2b8a7a 100644
--- a/src/modules/module-protocol-pulse/modules/module-ladspa-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-ladspa-source.c
@@ -59,7 +59,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_ladspa_source_load(struct client *client, struct module *module)
+static int module_ladspa_source_load(struct module *module)
 {
 	struct module_ladspa_source_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-loopback.c b/src/modules/module-protocol-pulse/modules/module-loopback.c
index 94d7fc70a5cc06dca1052970eba26b0455f1bb23..93e17bc1a973978936533bc0b455b305adfee572 100644
--- a/src/modules/module-protocol-pulse/modules/module-loopback.c
+++ b/src/modules/module-protocol-pulse/modules/module-loopback.c
@@ -63,7 +63,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_loopback_load(struct client *client, struct module *module)
+static int module_loopback_load(struct module *module)
 {
 	struct module_loopback_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c b/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c
index f97cac21b874863dbe15b1d42b47fee85f8ddd0e..e8f4c14cdc613b484a7de03c7981b4ccccbf3f10 100644
--- a/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c
+++ b/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c
@@ -38,10 +38,10 @@ struct module_native_protocol_tcp_data {
 	struct pw_array servers;
 };
 
-static int module_native_protocol_tcp_load(struct client *client, struct module *module)
+static int module_native_protocol_tcp_load(struct module *module)
 {
 	struct module_native_protocol_tcp_data *data = module->user_data;
-	struct impl *impl = client->impl;
+	struct impl *impl = module->impl;
 	const char *address;
 	int res;
 
diff --git a/src/modules/module-protocol-pulse/modules/module-null-sink.c b/src/modules/module-protocol-pulse/modules/module-null-sink.c
index c413c9932ac0724fb72b2788dac381908971dc26..caae59882c02876dff139b91308ae12707c14d24 100644
--- a/src/modules/module-protocol-pulse/modules/module-null-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-null-sink.c
@@ -104,11 +104,11 @@ static const struct pw_core_events core_events = {
 	.error = module_null_sink_core_error,
 };
 
-static int module_null_sink_load(struct client *client, struct module *module)
+static int module_null_sink_load(struct module *module)
 {
 	struct module_null_sink_data *d = module->user_data;
 
-	d->core = pw_context_connect(module->impl->context, pw_properties_copy(client->props), 0);
+	d->core = pw_context_connect(module->impl->context, NULL, 0);
 	if (d->core == NULL)
 		return -errno;
 
diff --git a/src/modules/module-protocol-pulse/modules/module-pipe-sink.c b/src/modules/module-protocol-pulse/modules/module-pipe-sink.c
index c1ea9a78cc24fed95b9d3178dc0c66e2fe6877ea..18027ac0c382ce9c4bb4ea666d054cec5151bf1f 100644
--- a/src/modules/module-protocol-pulse/modules/module-pipe-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-pipe-sink.c
@@ -64,7 +64,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_pipe_sink_load(struct client *client, struct module *module)
+static int module_pipe_sink_load(struct module *module)
 {
 	struct module_pipesink_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-pipe-source.c b/src/modules/module-protocol-pulse/modules/module-pipe-source.c
index 6827ee4fdaf764b3736a3af74e8848ff32bb42cd..f39e4eddb85d0ff80a17daa35971bf7e361a508e 100644
--- a/src/modules/module-protocol-pulse/modules/module-pipe-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-pipe-source.c
@@ -64,7 +64,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_pipe_source_load(struct client *client, struct module *module)
+static int module_pipe_source_load(struct module *module)
 {
 	struct module_pipesrc_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-raop-discover.c b/src/modules/module-protocol-pulse/modules/module-raop-discover.c
index fd6db7f0a1762091700fac252d66874a0cb5cb6a..bb93b024907cebac4aa37003f6bf9ecd5fea4e33 100644
--- a/src/modules/module-protocol-pulse/modules/module-raop-discover.c
+++ b/src/modules/module-protocol-pulse/modules/module-raop-discover.c
@@ -55,7 +55,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_raop_discover_load(struct client *client, struct module *module)
+static int module_raop_discover_load(struct module *module)
 {
 	struct module_raop_discover_data *data = module->user_data;
 
diff --git a/src/modules/module-protocol-pulse/modules/module-remap-sink.c b/src/modules/module-protocol-pulse/modules/module-remap-sink.c
index 5aed6f8a6e206a87383b188c7a296d794bd482d4..ffce605e1feda621845d4347f667f8a67e140716 100644
--- a/src/modules/module-protocol-pulse/modules/module-remap-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-remap-sink.c
@@ -59,7 +59,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_remap_sink_load(struct client *client, struct module *module)
+static int module_remap_sink_load(struct module *module)
 {
 	struct module_remap_sink_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-remap-source.c b/src/modules/module-protocol-pulse/modules/module-remap-source.c
index 33e770bd1eb59e503e50b449399f33343889c3f1..1f6d5a7bf8a447cb0d1271f1af29724219253c63 100644
--- a/src/modules/module-protocol-pulse/modules/module-remap-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-remap-source.c
@@ -59,7 +59,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_remap_source_load(struct client *client, struct module *module)
+static int module_remap_source_load(struct module *module)
 {
 	struct module_remap_source_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c b/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c
index bcbd65053190a82d610786b7609d29c94b8642ee..c1b2b17a77ab389e33cbeec35e687ab35fc7e54b 100644
--- a/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c
+++ b/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c
@@ -58,7 +58,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_roc_sink_input_load(struct client *client, struct module *module)
+static int module_roc_sink_input_load(struct module *module)
 {
 	struct module_roc_sink_input_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-sink.c b/src/modules/module-protocol-pulse/modules/module-roc-sink.c
index a50157a271b32470d78c1e241c7041fa4d7b2639..22ff4dc83f1eb0ac9d235210d5e2ebc878f27cc9 100644
--- a/src/modules/module-protocol-pulse/modules/module-roc-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-roc-sink.c
@@ -58,7 +58,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_roc_sink_load(struct client *client, struct module *module)
+static int module_roc_sink_load(struct module *module)
 {
 	struct module_roc_sink_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-source.c b/src/modules/module-protocol-pulse/modules/module-roc-source.c
index e99b24e076794a7303d244e251b473e6044a7e3e..b75e46e4dd531d81a8e0d0da2db30ae66a80f69c 100644
--- a/src/modules/module-protocol-pulse/modules/module-roc-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-roc-source.c
@@ -58,7 +58,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_roc_source_load(struct client *client, struct module *module)
+static int module_roc_source_load(struct module *module)
 {
 	struct module_roc_source_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-recv.c b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
index 29a0d70da38420b3f506530f7c6a948ef3056b97..59f6fa36f237d09a5c64e3c60ba112769b9b2d92 100644
--- a/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
+++ b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
@@ -57,7 +57,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_rtp_recv_load(struct client *client, struct module *module)
+static int module_rtp_recv_load(struct module *module)
 {
 	struct module_rtp_recv_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-send.c b/src/modules/module-protocol-pulse/modules/module-rtp-send.c
index 1f13e65075dcbcd50cc6e01beaa2a70cd4546128..743faaae67037f5f926fbbee4422976dd6118418 100644
--- a/src/modules/module-protocol-pulse/modules/module-rtp-send.c
+++ b/src/modules/module-protocol-pulse/modules/module-rtp-send.c
@@ -58,7 +58,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_rtp_send_load(struct client *client, struct module *module)
+static int module_rtp_send_load(struct module *module)
 {
 	struct module_rtp_send_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c b/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c
index 2cfcd237b039c4570292cf5204a5589e3eedeca9..3d0f7d780f4112e2d0d010a59f14416d60227b95 100644
--- a/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c
+++ b/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c
@@ -58,10 +58,10 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_simple_protocol_tcp_load(struct client *client, struct module *module)
+static int module_simple_protocol_tcp_load(struct module *module)
 {
 	struct module_simple_protocol_tcp_data *data = module->user_data;
-	struct impl *impl = client->impl;
+	struct impl *impl = module->impl;
 	char *args;
 	size_t size;
 	uint32_t i;
diff --git a/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c b/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c
index 7e30cd54d0adc82e92a130805268f252f4b41cb0..63541939c58b3708ce5adf7360886ee885838041 100644
--- a/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c
+++ b/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c
@@ -175,13 +175,13 @@ static const struct pw_core_events core_events = {
 	.done = on_core_done,
 };
 
-static int module_switch_on_connect_load(struct client *client, struct module *module)
+static int module_switch_on_connect_load(struct module *module)
 {
-	struct impl *impl = client->impl;
+	struct impl *impl = module->impl;
 	struct module_switch_on_connect_data *d = module->user_data;
 	int res;
 
-	d->core = pw_context_connect(impl->context, pw_properties_copy(client->props), 0);
+	d->core = pw_context_connect(impl->context, NULL, 0);
 	if (d->core == NULL) {
 		res = -errno;
 		goto error;
diff --git a/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c b/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c
index da4ec87d493ae0a864ad58c5a2ea7af5c1d6b4de..5089285e88a06cf9aeab8bfdcd9509d92a15ce61 100644
--- a/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c
@@ -62,7 +62,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_tunnel_sink_load(struct client *client, struct module *module)
+static int module_tunnel_sink_load(struct module *module)
 {
 	struct module_tunnel_sink_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-tunnel-source.c b/src/modules/module-protocol-pulse/modules/module-tunnel-source.c
index 7fa4b78e482733fe8ce4811ed38a74ba723c3578..e344754e79a4feff4d478d1b390e407ae8e22627 100644
--- a/src/modules/module-protocol-pulse/modules/module-tunnel-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-tunnel-source.c
@@ -62,7 +62,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_tunnel_source_load(struct client *client, struct module *module)
+static int module_tunnel_source_load(struct module *module)
 {
 	struct module_tunnel_source_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-x11-bell.c b/src/modules/module-protocol-pulse/modules/module-x11-bell.c
index a14c843a02fea598265cd4cca53ef568ee50d7e7..9d7e2176dd4423a6f14c5115cbb0c7adf6d8a318 100644
--- a/src/modules/module-protocol-pulse/modules/module-x11-bell.c
+++ b/src/modules/module-protocol-pulse/modules/module-x11-bell.c
@@ -51,7 +51,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_x11_bell_load(struct client *client, struct module *module)
+static int module_x11_bell_load(struct module *module)
 {
 	struct module_x11_bell_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c b/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
index 79f14e853c2433b2286ad9742e3b7c91e5ec7c81..c44c6c8677779ab6615602d0728a1a4e7b825b50 100644
--- a/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
+++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
@@ -57,7 +57,7 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy
 };
 
-static int module_zeroconf_discover_load(struct client *client, struct module *module)
+static int module_zeroconf_discover_load(struct module *module)
 {
 	struct module_zeroconf_discover_data *data = module->user_data;
 	FILE *f;
diff --git a/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
index 8da1b6ae83edf518bc5264fdccae07e554c6995b..2f048678da34c693ec84b8ca0ffae5e054ae5226 100644
--- a/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
+++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
@@ -73,6 +73,7 @@ struct service {
 
 	AvahiEntryGroup *entry_group;
 	AvahiStringList *txt;
+	struct server *server;
 
 	const char *service_type;
 	enum service_subtype subtype;
@@ -154,6 +155,7 @@ static void unpublish_service(struct service *s)
 	spa_list_remove(&s->link);
 	spa_list_append(&s->userdata->pending, &s->link);
 	s->published = false;
+	s->server = NULL;
 }
 
 static void unpublish_all_services(struct module_zeroconf_publish_data *d)
@@ -397,7 +399,7 @@ static AvahiStringList *get_service_txt(const struct service *s)
 	return txt;
 }
 
-static int find_port(struct service *s, int *proto, uint16_t *port)
+static struct server *find_server(struct service *s, int *proto, uint16_t *port)
 {
 	struct module_zeroconf_publish_data *d = s->userdata;
 	struct impl *impl = d->module->impl;
@@ -407,14 +409,15 @@ static int find_port(struct service *s, int *proto, uint16_t *port)
 		if (server->addr.ss_family == AF_INET) {
 			*proto = AVAHI_PROTO_INET;
 			*port = ntohs(((struct sockaddr_in*) &server->addr)->sin_port);
-			return 0;
+			return server;
 		} else if (server->addr.ss_family == AF_INET6) {
 			*proto = AVAHI_PROTO_INET6;
 			*port = ntohs(((struct sockaddr_in6*) &server->addr)->sin6_port);
-			return 0;
+			return server;
 		}
 	}
-	return -ENODEV;
+
+	return NULL;
 }
 
 static void publish_service(struct service *s)
@@ -423,10 +426,11 @@ static void publish_service(struct service *s)
 	int proto;
 	uint16_t port;
 
-	if (find_port(s, &proto, &port) < 0)
+	struct server *server = find_server(s, &proto, &port);
+	if (!server)
 		return;
 
-	pw_log_debug("found proto:%d port:%d", proto, port);
+	pw_log_debug("found server:%p proto:%d port:%d", server, proto, port);
 
 	if (!d->client || avahi_client_get_state(d->client) != AVAHI_CLIENT_S_RUNNING)
 		return;
@@ -499,6 +503,7 @@ static void publish_service(struct service *s)
 
 	spa_list_remove(&s->link);
 	spa_list_append(&d->published, &s->link);
+	s->server = server;
 
 	pw_log_info("created service: %s", s->service_name);
 	return;
@@ -626,7 +631,13 @@ static void impl_server_stopped(void *data, struct server *server)
 {
 	struct module_zeroconf_publish_data *d = data;
 	pw_log_info("a server stopped, try republish");
-	unpublish_all_services(d);
+
+	struct service *s, *tmp;
+	spa_list_for_each_safe(s, tmp, &d->published, link) {
+		if (s->server == server)
+			unpublish_service(s);
+	}
+
 	publish_pending(d);
 }
 
@@ -636,14 +647,13 @@ static const struct impl_events impl_events = {
 	.server_stopped = impl_server_stopped,
 };
 
-static int module_zeroconf_publish_load(struct client *client, struct module *module)
+static int module_zeroconf_publish_load(struct module *module)
 {
 	struct module_zeroconf_publish_data *data = module->user_data;
 	struct pw_loop *loop;
 	int error;
 
-	data->core = pw_context_connect(module->impl->context,
-			pw_properties_copy(client->props), 0);
+	data->core = pw_context_connect(module->impl->context, NULL, 0);
 	if (data->core == NULL) {
 		pw_log_error("failed to connect to pipewire: %m");
 		return -errno;
diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c
index 84bf3b56c5299aed0fc905073f35a9258f6fdfc7..97405e03985ec9fa15b023f9c469618d1a973ccc 100644
--- a/src/modules/module-protocol-pulse/pulse-server.c
+++ b/src/modules/module-protocol-pulse/pulse-server.c
@@ -57,6 +57,7 @@
 #include "client.h"
 #include "collect.h"
 #include "commands.h"
+#include "cmd.h"
 #include "dbus-name.h"
 #include "defs.h"
 #include "extension.h"
@@ -85,7 +86,7 @@
 #define DEFAULT_MIN_QUANTUM	"256/48000"
 #define DEFAULT_FORMAT		"F32"
 #define DEFAULT_POSITION	"[ FL FR ]"
-#define DEFAULT_IDLE_TIMEOUT	"5"
+#define DEFAULT_IDLE_TIMEOUT	"0"
 
 #define MAX_FORMATS	32
 /* The max amount of data we send in one block when capturing. In PulseAudio this
@@ -94,6 +95,8 @@
 
 #define TEMPORARY_MOVE_TIMEOUT	(SPA_NSEC_PER_SEC)
 
+PW_LOG_TOPIC_EXTERN(pulse_conn);
+
 bool debug_messages = false;
 
 struct latency_offset_data {
@@ -5131,6 +5134,7 @@ static int do_load_module(struct client *client, uint32_t command, uint32_t tag,
 		.sync = on_load_module_manager_sync,
 	};
 
+	struct impl *impl = client->impl;
 	const char *name, *argument;
 	struct module *module;
 	struct pending_module *pm;
@@ -5145,7 +5149,7 @@ static int do_load_module(struct client *client, uint32_t command, uint32_t tag,
 	pw_log_info("[%s] %s name:%s argument:%s",
 			client->name, commands[command].name, name, argument);
 
-	module = module_create(client, name, argument);
+	module = module_create(impl, name, argument);
 	if (module == NULL)
 		return -errno;
 
@@ -5159,7 +5163,7 @@ static int do_load_module(struct client *client, uint32_t command, uint32_t tag,
 
 	pw_log_debug("pending module %p: start tag:%d", pm, tag);
 
-	r = module_load(client, module);
+	r = module_load(module);
 
 	module_add_listener(module, &pm->module_listener, &module_events, pm);
 	client_add_listener(client, &pm->client_listener, &client_events, pm);
@@ -5624,7 +5628,7 @@ struct pw_protocol_pulse *pw_protocol_pulse_new(struct pw_context *context,
 
 	load_defaults(&impl->defs, props);
 
-	debug_messages = pw_debug_is_category_enabled("connection");
+	debug_messages = pw_log_topic_enabled(SPA_LOG_LEVEL_INFO, pulse_conn);
 
 	impl->context = context;
 	impl->loop = pw_context_get_main_loop(context);
@@ -5669,6 +5673,7 @@ struct pw_protocol_pulse *pw_protocol_pulse_new(struct pw_context *context,
 #ifdef HAVE_DBUS
 	impl->dbus_name = dbus_request_name(context, "org.pulseaudio.Server");
 #endif
+	cmd_run(impl);
 
 	return (struct pw_protocol_pulse *) impl;
 
diff --git a/src/modules/module-protocol-pulse/server.c b/src/modules/module-protocol-pulse/server.c
index 503746f8f83dbbff1e1e1c5b9cd70720e92ba0f9..927e25327641a12a7ed7101bc80f8c0a5b0d9d0b 100644
--- a/src/modules/module-protocol-pulse/server.c
+++ b/src/modules/module-protocol-pulse/server.c
@@ -823,13 +823,14 @@ static int parse_ip_address(const char *address, struct sockaddr_storage *addrs,
 	if (len < 2)
 		return -ENOSPC;
 
-	snprintf(ip, sizeof(ip), "[::]:%d", res);
-	spa_assert_se(parse_ipv6_address(ip, (struct sockaddr_in6 *) &addr) == 0);
-	addrs[0] = addr;
-
 	snprintf(ip, sizeof(ip), "0.0.0.0:%d", res);
 	spa_assert_se(parse_ipv4_address(ip, (struct sockaddr_in *) &addr) == 0);
+	addrs[0] = addr;
+
+	snprintf(ip, sizeof(ip), "[::]:%d", res);
+	spa_assert_se(parse_ipv6_address(ip, (struct sockaddr_in6 *) &addr) == 0);
 	addrs[1] = addr;
+
 	return 2;
 }
 
diff --git a/src/modules/module-pulse-tunnel.c b/src/modules/module-pulse-tunnel.c
index 2c9a97df781fd21130ead6e3754dff392fbd2309..916400d22be272f914591be69f217556fa15ab33 100644
--- a/src/modules/module-pulse-tunnel.c
+++ b/src/modules/module-pulse-tunnel.c
@@ -189,6 +189,7 @@ struct impl {
 	uint32_t target_latency;
 	uint32_t current_latency;
 	uint32_t target_buffer;
+	struct spa_io_rate_match *rate_match;
 	struct spa_dll dll;
 	float max_error;
 	unsigned resync:1;
@@ -250,6 +251,28 @@ static void stream_state_changed(void *d, enum pw_stream_state old,
 	}
 }
 
+static void update_rate(struct impl *impl, bool playback)
+{
+	float error, corr;
+
+	if (impl->rate_match == NULL)
+		return;
+
+	if (playback)
+		error = (float)impl->target_latency - (float)impl->current_latency;
+	else
+		error = (float)impl->current_latency - (float)impl->target_latency;
+	error = SPA_CLAMP(error, -impl->max_error, impl->max_error);
+
+	corr = spa_dll_update(&impl->dll, error);
+	pw_log_debug("error:%f corr:%f current:%u target:%u",
+			error, corr,
+			impl->current_latency, impl->target_latency);
+
+	SPA_FLAG_SET(impl->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE);
+	impl->rate_match->rate = corr;
+}
+
 static void playback_stream_process(void *d)
 {
 	struct impl *impl = d;
@@ -279,17 +302,7 @@ static void playback_stream_process(void *d)
                                         size, RINGBUFFER_SIZE);
 		impl->resync = true;
 	} else {
-		float error, corr;
-
-		error = (float)impl->target_latency - (float)impl->current_latency;
-		error = SPA_CLAMP(error, -impl->max_error, impl->max_error);
-
-		corr = spa_dll_update(&impl->dll, error);
-		pw_log_debug("filled:%u target:%u error:%f corr:%f %u %u", filled,
-				impl->target_buffer, error, corr,
-				impl->current_latency, impl->target_latency);
-		pw_stream_set_control(impl->stream,
-				SPA_PROP_rate, 1, &corr, NULL);
+		update_rate(impl, true);
 	}
 	spa_ringbuffer_write_data(&impl->ring,
 				impl->buffer, RINGBUFFER_SIZE,
@@ -326,24 +339,12 @@ static void capture_stream_process(void *d)
 	if (avail < (int32_t)size) {
 		memset(bd->data, 0, size);
 	} else {
-		float error, corr;
-
 		if (avail > (int32_t)RINGBUFFER_SIZE) {
 			avail = impl->target_buffer;
 			index += avail - impl->target_buffer;
 		} else {
-			error = (float)(impl->current_latency) - (float)impl->target_latency;
-			error = SPA_CLAMP(error, -impl->max_error, impl->max_error);
-
-			corr = spa_dll_update(&impl->dll, error);
-
-			pw_log_debug("avail:%u target:%u error:%f corr:%f %u %u", avail,
-					impl->target_buffer, error, corr,
-					impl->current_latency, impl->target_latency);
-			pw_stream_set_control(impl->stream,
-					SPA_PROP_rate, 1, &corr, NULL);
+			update_rate(impl, false);
 		}
-
 		spa_ringbuffer_read_data(&impl->ring,
 				impl->buffer, RINGBUFFER_SIZE,
 				index & RINGBUFFER_MASK,
@@ -359,10 +360,21 @@ static void capture_stream_process(void *d)
 	pw_stream_queue_buffer(impl->stream, buf);
 }
 
+static void stream_io_changed(void *data, uint32_t id, void *area, uint32_t size)
+{
+	struct impl *impl = data;
+	switch (id) {
+	case SPA_IO_RateMatch:
+		impl->rate_match = area;
+		break;
+	}
+}
+
 static const struct pw_stream_events playback_stream_events = {
 	PW_VERSION_STREAM_EVENTS,
 	.destroy = stream_destroy,
 	.state_changed = stream_state_changed,
+	.io_changed = stream_io_changed,
 	.process = playback_stream_process
 };
 
@@ -370,6 +382,7 @@ static const struct pw_stream_events capture_stream_events = {
 	PW_VERSION_STREAM_EVENTS,
 	.destroy = stream_destroy,
 	.state_changed = stream_state_changed,
+	.io_changed = stream_io_changed,
 	.process = capture_stream_process
 };
 
diff --git a/src/modules/module-rtp-sink.c b/src/modules/module-rtp-sink.c
index 014446d5c057951ef913cac94e84dd1a32e27715..8a00848eccfdc15b6ea89778cfb6290f75135552 100644
--- a/src/modules/module-rtp-sink.c
+++ b/src/modules/module-rtp-sink.c
@@ -306,8 +306,17 @@ static void flush_packets(struct impl *impl)
 			&iov[1], tosend);
 
 		n = sendmsg(impl->rtp_fd, &msg, MSG_NOSIGNAL);
-		if (n < 0)
-			pw_log_warn("sendmsg() failed: %m");
+		if (n < 0) {
+			switch (errno) {
+			case ECONNREFUSED:
+			case ECONNRESET:
+				pw_log_debug("remote end not listening");
+				break;
+			default:
+				pw_log_warn("sendmsg() failed: %m");
+				break;
+			}
+		}
 
 		impl->seq++;
 		impl->timestamp += tosend / impl->frame_size;
diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c
index 503bfc1961b064cafab2fbf78b511fd9ac051a90..bc25cdc3db53463cf43339f2640268acf6f3b010 100644
--- a/src/modules/module-rtp-source.c
+++ b/src/modules/module-rtp-source.c
@@ -214,6 +214,7 @@ struct session {
 	struct spa_ringbuffer ring;
 	uint8_t buffer[BUFFER_SIZE];
 
+	struct spa_io_rate_match *rate_match;
 	struct spa_dll dll;
 	uint32_t target_buffer;
 	float max_error;
@@ -269,8 +270,10 @@ static void stream_process(void *data)
 			pw_log_debug("avail:%u target:%u error:%f corr:%f", avail,
 					sess->target_buffer, error, corr);
 
-			pw_stream_set_control(sess->stream,
-					SPA_PROP_rate, 1, &corr, NULL);
+			if (sess->rate_match) {
+				SPA_FLAG_SET(sess->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE);
+				sess->rate_match->rate = corr;
+			}
 		}
 		spa_ringbuffer_read_data(&sess->ring,
 				sess->buffer,
@@ -308,10 +311,21 @@ static void on_stream_state_changed(void *d, enum pw_stream_state old,
 	}
 }
 
+static void stream_io_changed(void *data, uint32_t id, void *area, uint32_t size)
+{
+	struct session *sess = data;
+	switch (id) {
+	case SPA_IO_RateMatch:
+		sess->rate_match = area;
+		break;
+	}
+}
+
 static const struct pw_stream_events out_stream_events = {
 	PW_VERSION_STREAM_EVENTS,
 	.destroy = stream_destroy,
 	.state_changed = on_stream_state_changed,
+	.io_changed = stream_io_changed,
 	.process = stream_process
 };
 
diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c
index 5eeff5b01dcfc799da18dd8cd7d55a8aa6791dbf..f121ce83aab340134441f9bf08f03ed54d03497b 100644
--- a/src/pipewire/impl-link.c
+++ b/src/pipewire/impl-link.c
@@ -622,8 +622,8 @@ int pw_impl_link_activate(struct pw_impl_link *this)
 	pw_log_debug("%p: activate activated:%d state:%s", this, impl->activated,
 			pw_link_state_as_string(this->info.state));
 
-	if (impl->activated || !this->prepared || !impl->inode->active ||
-			!impl->inode->added || !impl->onode->active)
+	if (impl->activated || !this->prepared ||
+		!impl->inode->active || !impl->onode->active)
 		return 0;
 
 	if (!impl->io_set) {
@@ -812,7 +812,7 @@ do_deactivate_link(struct spa_loop *loop,
 	spa_list_remove(&this->rt.out_mix.rt_link);
 	spa_list_remove(&this->rt.in_mix.rt_link);
 
-	if (this->input->node != this->output->node) {
+	if (impl->inode != impl->onode) {
 		struct pw_node_activation_state *state;
 
 		spa_list_remove(&this->rt.target.link);
diff --git a/src/pipewire/impl-node.c b/src/pipewire/impl-node.c
index 5480d273e1c4274fd84dd43a7ddb9bae2d013770..dd20620a94c5e5145f7285d20e61faf23e6d1580 100644
--- a/src/pipewire/impl-node.c
+++ b/src/pipewire/impl-node.c
@@ -158,12 +158,51 @@ static void remove_node(struct pw_impl_node *this)
 	this->rt.driver_target.node = NULL;
 }
 
+static int
+do_node_add(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+	struct pw_impl_node *this = user_data;
+	struct pw_impl_node *driver = this->driver_node;
+
+	this->added = true;
+	if (this->source.loop == NULL) {
+		struct spa_system *data_system = this->context->data_system;
+		uint64_t dummy;
+		int res;
+
+		/* clear the eventfd in case it was written to while the node was stopped */
+		res = spa_system_eventfd_read(data_system, this->source.fd, &dummy);
+		if (SPA_UNLIKELY(res != -EAGAIN && res != 0))
+			pw_log_warn("%p: read failed %m", this);
+
+		spa_loop_add_source(loop, &this->source);
+		add_node(this, driver);
+	}
+	return 0;
+}
+
+static int
+do_node_remove(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+	struct pw_impl_node *this = user_data;
+	if (this->source.loop != NULL) {
+		spa_loop_remove_source(loop, &this->source);
+		remove_node(this);
+	}
+	this->added = false;
+	return 0;
+}
+
 static void node_deactivate(struct pw_impl_node *this)
 {
 	struct pw_impl_port *port;
 	struct pw_impl_link *link;
 
 	pw_log_debug("%p: deactivate", this);
+
+	/* make sure the node doesn't get woken up while not active */
+	pw_loop_invoke(this->data_loop, do_node_remove, 1, NULL, 0, true, this);
+
 	spa_list_for_each(port, &this->input_ports, link) {
 		spa_list_for_each(link, &port->links, input_link)
 			pw_impl_link_deactivate(link);
@@ -200,7 +239,7 @@ static int idle_node(struct pw_impl_node *this)
 	return res;
 }
 
-static void node_activate_outputs(struct pw_impl_node *this)
+static void node_activate(struct pw_impl_node *this)
 {
 	struct pw_impl_port *port;
 
@@ -210,13 +249,6 @@ static void node_activate_outputs(struct pw_impl_node *this)
 		spa_list_for_each(link, &port->links, output_link)
 			pw_impl_link_activate(link);
 	}
-}
-
-static void node_activate_inputs(struct pw_impl_node *this)
-{
-	struct pw_impl_port *port;
-
-	pw_log_debug("%p: activate", this);
 	spa_list_for_each(port, &this->input_ports, link) {
 		struct pw_impl_link *link;
 		spa_list_for_each(link, &port->links, input_link)
@@ -229,9 +261,7 @@ static int start_node(struct pw_impl_node *this)
 	struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
 	int res = 0;
 
-	/* First activate the outputs so that when the node starts pushing,
-	 * we can process the outputs */
-	node_activate_outputs(this);
+	node_activate(this);
 
 	if (impl->pending_state >= PW_NODE_STATE_RUNNING)
 		return 0;
@@ -243,6 +273,10 @@ static int start_node(struct pw_impl_node *this)
 		impl->pending_play = true;
 		res = spa_node_send_command(this->node,
 			&SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start));
+	} else {
+		/* driver nodes will wait until all other nodes are started before
+		 * they are started */
+		res = EBUSY;
 	}
 
 	if (res < 0)
@@ -325,34 +359,6 @@ static void emit_params(struct pw_impl_node *node, uint32_t *changed_ids, uint32
 	}
 }
 
-static int
-do_node_add(struct spa_loop *loop,
-	    bool async, uint32_t seq, const void *data, size_t size, void *user_data)
-{
-	struct pw_impl_node *this = user_data;
-	struct pw_impl_node *driver = this->driver_node;
-
-	if (this->source.loop == NULL) {
-		spa_loop_add_source(loop, &this->source);
-		add_node(this, driver);
-	}
-	this->added = true;
-	return 0;
-}
-
-static int
-do_node_remove(struct spa_loop *loop,
-	       bool async, uint32_t seq, const void *data, size_t size, void *user_data)
-{
-	struct pw_impl_node *this = user_data;
-	if (this->source.loop != NULL) {
-		spa_loop_remove_source(loop, &this->source);
-		remove_node(this);
-	}
-	this->added = false;
-	return 0;
-}
-
 static void node_update_state(struct pw_impl_node *node, enum pw_node_state state, int res, char *error)
 {
 	struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
@@ -375,10 +381,6 @@ static void node_update_state(struct pw_impl_node *node, enum pw_node_state stat
 				pw_loop_invoke(node->data_loop, do_node_remove, 1, NULL, 0, true, node);
 			}
 		}
-		if (res >= 0) {
-			/* now activate the inputs */
-			node_activate_inputs(node);
-		}
 		break;
 	case PW_NODE_STATE_IDLE:
 	case PW_NODE_STATE_SUSPENDED:
@@ -807,17 +809,9 @@ do_move_nodes(struct spa_loop *loop,
 	struct impl *impl = user_data;
 	struct pw_impl_node *driver = *(struct pw_impl_node **)data;
 	struct pw_impl_node *node = &impl->this;
-	int res;
 
 	pw_log_trace("%p: driver:%p->%p", node, node->driver_node, driver);
 
-	if ((res = spa_node_set_io(node->node,
-		    SPA_IO_Position,
-		    &driver->rt.activation->position,
-		    sizeof(struct spa_io_position))) < 0) {
-		pw_log_debug("%p: set position: %s", node, spa_strerror(res));
-	}
-
 	pw_log_trace("%p: set position %p", node, &driver->rt.activation->position);
 	node->rt.position = &driver->rt.activation->position;
 
@@ -843,6 +837,8 @@ int pw_impl_node_set_driver(struct pw_impl_node *node, struct pw_impl_node *driv
 {
 	struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
 	struct pw_impl_node *old = node->driver_node;
+	int res;
+	bool was_driving;
 
 	if (driver == NULL)
 		driver = node;
@@ -865,8 +861,16 @@ int pw_impl_node_set_driver(struct pw_impl_node *node, struct pw_impl_node *driv
 				old->name, old->info.id,
 				driver->name, driver->info.id);
 	}
+	was_driving = node->driving;
 	node->driving = node->driver && driver == node;
 
+	/* When a node was driver (and is waiting for all nodes to complete
+	 * the Start command) cancel the pending state and let the new driver
+	 * calculate a new state so that the Start command is sent to the
+	 * node */
+	if (was_driving && !node->driving)
+		impl->pending_state = node->info.state;
+
 	pw_log_debug("%p: driver %p driving:%u", node,
 		driver, node->driving);
 	pw_log_info("(%s-%u) -> change driver (%s-%d -> %s-%d)",
@@ -876,6 +880,13 @@ int pw_impl_node_set_driver(struct pw_impl_node *node, struct pw_impl_node *driv
 	node->driver_node = driver;
 	node->moved = true;
 
+	if ((res = spa_node_set_io(node->node,
+		    SPA_IO_Position,
+		    &driver->rt.activation->position,
+		    sizeof(struct spa_io_position))) < 0) {
+		pw_log_debug("%p: set position: %s", node, spa_strerror(res));
+	}
+
 	pw_loop_invoke(node->data_loop,
 		       do_move_nodes, SPA_ID_INVALID, &driver, sizeof(struct pw_impl_node *),
 		       true, impl);
@@ -1110,28 +1121,30 @@ static inline int process_node(void *data)
 	a->status = PW_NODE_ACTIVATION_AWAKE;
 	a->awake_time = SPA_TIMESPEC_TO_NSEC(&ts);
 
-	if (!this->added) {
-		/* This should not happen here. We activate the input
-		 * links after we add the node to the graph. */
-		pw_log_warn("%p: scheduling non-active node", this);
-		return -EIO;
-	}
 	pw_log_trace_fp("%p: process %"PRIu64, this, a->awake_time);
 
 	/* when transport sync is not supported, just clear the flag */
 	if (!this->transport_sync)
 		a->pending_sync = false;
 
-	spa_list_for_each(p, &this->rt.input_mix, rt.node_link)
-		spa_node_process(p->mix);
+	if (this->added) {
+		spa_list_for_each(p, &this->rt.input_mix, rt.node_link)
+			spa_node_process(p->mix);
 
-	status = spa_node_process(this->node);
-	a->state[0].status = status;
+		status = spa_node_process(this->node);
 
-	if (status & SPA_STATUS_HAVE_DATA) {
-		spa_list_for_each(p, &this->rt.output_mix, rt.node_link)
-			spa_node_process(p->mix);
+		if (status & SPA_STATUS_HAVE_DATA) {
+			spa_list_for_each(p, &this->rt.output_mix, rt.node_link)
+				spa_node_process(p->mix);
+		}
+	} else {
+		/* This can happen when we deactivated the node but some links are
+		 * still not shut down. We simply don't schedule the node and make
+		 * sure we trigger the peers in resume_node below. */
+		pw_log_debug("%p: scheduling non-active node %s", this, this->name);
+		status = SPA_STATUS_HAVE_DATA;
 	}
+	a->state[0].status = status;
 
 	if (SPA_UNLIKELY(this == this->driver_node && !this->exported)) {
 		spa_system_clock_gettime(data_system, CLOCK_MONOTONIC, &ts);
@@ -1636,7 +1649,11 @@ static int node_ready(void *data, int status)
 			node->driver, node->exported, driver, status, node->added);
 
 	if (!node->added) {
-		pw_log_warn("%p: ready non-active node", node);
+		/* This can happen when we are stopping a node and removed it from the
+		 * graph but we still have not completed the Pause/Suspend command on
+		 * the node. In that case, the node might still emit ready events,
+		 * which we should simply ignore here. */
+		pw_log_info("%p: ready non-active node %s in state %d", node, node->name, node->info.state);
 		return -EIO;
 	}
 
@@ -2155,6 +2172,10 @@ static void on_state_complete(void *obj, void *data, int res, uint32_t seq)
 	enum pw_node_state state = SPA_PTR_TO_INT(data);
 	char *error = NULL;
 
+	/* driver nodes added -EBUSY. This is then not an error */
+	if (res == -EBUSY)
+		res = 0;
+
 	impl->pending_id = SPA_ID_INVALID;
 	impl->pending_play = false;
 
@@ -2242,9 +2263,13 @@ int pw_impl_node_set_state(struct pw_impl_node *node, enum pw_node_state state)
 			pw_work_queue_cancel(impl->work, node, impl->pending_id);
 			node->info.state = impl->pending_state;
 		}
+		/* driver nodes return EBUSY to add a -EBUSY to the work queue. This
+		 * will wait until all previous items in the work queue are
+		 * completed */
 		impl->pending_state = state;
 		impl->pending_id = pw_work_queue_add(impl->work,
-				node, res, on_state_complete, SPA_INT_TO_PTR(state));
+				node, res == EBUSY ? -EBUSY : res,
+				on_state_complete, SPA_INT_TO_PTR(state));
 	}
 	return res;
 }
@@ -2255,9 +2280,9 @@ int pw_impl_node_set_active(struct pw_impl_node *node, bool active)
 	bool old = node->active;
 
 	if (old != active) {
-		pw_log_debug("%p: %s registered:%d", node,
+		pw_log_debug("%p: %s registered:%d exported:%d", node,
 				active ? "activate" : "deactivate",
-				node->registered);
+				node->registered, node->exported);
 
 		node->active = active;
 		pw_impl_node_emit_active_changed(node, active);
diff --git a/src/pipewire/pipewire.c b/src/pipewire/pipewire.c
index 255760c90df8ec5206905def6d341c7379044f57..cc5610ed80ee241760acb2850e104fd5d682fd66 100644
--- a/src/pipewire/pipewire.c
+++ b/src/pipewire/pipewire.c
@@ -64,7 +64,6 @@ struct plugin {
 	char *filename;
 	void *hnd;
 	spa_handle_factory_enum_func_t enum_func;
-	struct spa_list handles;
 	int ref;
 };
 
@@ -78,10 +77,10 @@ struct handle {
 
 struct registry {
 	struct spa_list plugins;
+	struct spa_list handles; /* all handles across all plugins by age (youngest first) */
 };
 
 struct support {
-	char **categories;
 	const char *plugin_dir;
 	const char *support_lib;
 	struct registry registry;
@@ -93,6 +92,7 @@ struct support {
 	unsigned int in_valgrind:1;
 	unsigned int no_color:1;
 	unsigned int no_config:1;
+	unsigned int do_dlclose:1;
 };
 
 static pthread_mutex_t init_lock = PTHREAD_MUTEX_INITIALIZER;
@@ -149,7 +149,6 @@ open_plugin(struct registry *registry,
 	plugin->filename = strdup(filename);
 	plugin->hnd = hnd;
 	plugin->enum_func = enum_func;
-	spa_list_init(&plugin->handles);
 
 	spa_list_append(&registry->plugins, &plugin->link);
 
@@ -168,7 +167,7 @@ unref_plugin(struct plugin *plugin)
 	if (--plugin->ref == 0) {
 		spa_list_remove(&plugin->link);
 		pw_log_debug("unloaded plugin:'%s'", plugin->filename);
-		if (!global_support.in_valgrind)
+		if (global_support.do_dlclose)
 			dlclose(plugin->hnd);
 		free(plugin->filename);
 		free(plugin);
@@ -290,7 +289,7 @@ static struct spa_handle *load_spa_handle(const char *lib,
 	handle->ref = 1;
 	handle->plugin = plugin;
 	handle->factory_name = strdup(factory_name);
-	spa_list_append(&plugin->handles, &handle->link);
+	spa_list_prepend(&sup->registry.handles, &handle->link);
 
 	return &handle->handle;
 
@@ -321,15 +320,13 @@ struct spa_handle *pw_load_spa_handle(const char *lib,
 static struct handle *find_handle(struct spa_handle *handle)
 {
 	struct registry *registry = &global_support.registry;
-	struct plugin *p;
 	struct handle *h;
 
-	spa_list_for_each(p, &registry->plugins, link) {
-		spa_list_for_each(h, &p->handles, link) {
-			if (&h->handle == handle)
-				return h;
-		}
+	spa_list_for_each(h, &registry->handles, link) {
+		if (&h->handle == handle)
+			return h;
 	}
+
 	return NULL;
 }
 
@@ -490,22 +487,30 @@ static struct spa_log *load_journal_logger(struct support *support,
 }
 #endif
 
-static enum spa_log_level
-parse_log_level(const char *str)
-{
-	enum spa_log_level l = SPA_LOG_LEVEL_NONE;
-	switch(str[0]) {
-		case 'X': l = SPA_LOG_LEVEL_NONE; break;
-		case 'E': l = SPA_LOG_LEVEL_ERROR; break;
-		case 'W': l = SPA_LOG_LEVEL_WARN; break;
-		case 'I': l = SPA_LOG_LEVEL_INFO; break;
-		case 'D': l = SPA_LOG_LEVEL_DEBUG; break;
-		case 'T': l = SPA_LOG_LEVEL_TRACE; break;
+static bool
+parse_log_level(const char *str, enum spa_log_level *l)
+{
+	uint32_t lvl;
+	if (strlen(str) == 1) {
+		switch(str[0]) {
+		case 'X': lvl = SPA_LOG_LEVEL_NONE; break;
+		case 'E': lvl = SPA_LOG_LEVEL_ERROR; break;
+		case 'W': lvl = SPA_LOG_LEVEL_WARN; break;
+		case 'I': lvl = SPA_LOG_LEVEL_INFO; break;
+		case 'D': lvl = SPA_LOG_LEVEL_DEBUG; break;
+		case 'T': lvl = SPA_LOG_LEVEL_TRACE; break;
 		default:
-			  l = atoi(str);
-			  break;
+			  goto check_int;
+		}
+	} else {
+check_int:
+		  if (!spa_atou32(str, &lvl, 0))
+			  return false;
+		  if (lvl > SPA_LOG_LEVEL_TRACE)
+			  return false;
 	}
-	return l;
+	*l = lvl;
+	return true;
 }
 
 static char *
@@ -518,6 +523,7 @@ parse_pw_debug_env(void)
 	char json[1024] = {0};
 	char *pos = json;
 	char *end = pos + sizeof(json) - 1;
+	enum spa_log_level lvl;
 
 	str = getenv("PIPEWIRE_DEBUG");
 
@@ -530,13 +536,6 @@ parse_pw_debug_env(void)
 	 */
 	pos += spa_scnprintf(pos, end - pos, "[ { conn.* = %d },", SPA_LOG_LEVEL_NONE);
 
-	/* We only have single-digit log levels, so any single-character
-	 * string is of the form PIPEWIRE_DEBUG=<N> */
-	if (slen == 1) {
-		pw_log_set_level(parse_log_level(str));
-		goto out;
-	}
-
 	tokens = pw_split_strv(str, ",", INT_MAX, &n_tokens);
 	if (n_tokens > 0) {
 		int i;
@@ -544,24 +543,23 @@ parse_pw_debug_env(void)
 			int n_tok;
 			char **tok;
 			char *pattern;
-			enum spa_log_level lvl;
 
 			tok = pw_split_strv(tokens[i], ":", 2, &n_tok);
-			if (n_tok == 2) {
+			if (n_tok == 2 && parse_log_level(tok[1], &lvl)) {
 				pattern = tok[0];
-				lvl = parse_log_level(tok[1]);
-
 				pos += spa_scnprintf(pos, end - pos, "{ %s = %d },",
 						     pattern, lvl);
+			} else if (n_tok == 1 && parse_log_level(tok[0], &lvl)) {
+				pw_log_set_level(lvl);
 			} else {
-				pw_log_warn("Ignoring invalid format in PIPEWIRE_DEBUG: '%s'\n", tokens[i]);
+				pw_log_warn("Ignoring invalid format in PIPEWIRE_DEBUG: '%s'",
+						tokens[i]);
 			}
 
 			pw_free_strv(tok);
 		}
 	}
 	pw_free_strv(tokens);
-out:
 	pos += spa_scnprintf(pos, end - pos, "]");
 	return strdup(json);
 }
@@ -594,6 +592,10 @@ void pw_init(int *argc, char **argv[])
 	pthread_mutex_lock(&support_lock);
 	support->in_valgrind = RUNNING_ON_VALGRIND;
 
+	support->do_dlclose = true;
+	if ((str = getenv("PIPEWIRE_DLCLOSE")) != NULL)
+		support->do_dlclose = pw_properties_parse_bool(str);
+
 	if (getenv("NO_COLOR") != NULL)
 		support->no_color = true;
 
@@ -611,6 +613,7 @@ void pw_init(int *argc, char **argv[])
 	support->support_lib = str;
 
 	spa_list_init(&support->registry.plugins);
+	spa_list_init(&support->registry.handles);
 
 	if (pw_log_is_default()) {
 		char *patterns = NULL;
@@ -684,7 +687,7 @@ void pw_deinit(void)
 {
 	struct support *support = &global_support;
 	struct registry *registry = &support->registry;
-	struct plugin *p;
+	struct handle *h;
 
 	pthread_mutex_lock(&init_lock);
 	if (support->init_count == 0)
@@ -694,14 +697,10 @@ void pw_deinit(void)
 
 	pthread_mutex_lock(&support_lock);
 	pw_log_set(NULL);
-	spa_list_consume(p, &registry->plugins, link) {
-		struct handle *h;
-		p->ref++;
-		spa_list_consume(h, &p->handles, link)
-			unref_handle(h);
-		unref_plugin(p);
-	}
-	pw_free_strv(support->categories);
+
+	spa_list_consume(h, &registry->handles, link)
+		unref_handle(h);
+
 	free(support->i18n_domain);
 	spa_zero(global_support);
 	pthread_mutex_unlock(&support_lock);
@@ -721,16 +720,9 @@ done:
 SPA_EXPORT
 bool pw_debug_is_category_enabled(const char *name)
 {
-	int i;
-
-	if (global_support.categories == NULL)
-		return false;
-
-	for (i = 0; global_support.categories[i]; i++) {
-		if (spa_streq(global_support.categories[i], name))
-			return true;
-	}
-	return false;
+	struct spa_log_topic t = SPA_LOG_TOPIC(0, name);
+	PW_LOG_TOPIC_INIT(&t);
+	return t.has_custom_level;
 }
 
 /** Get the application name */
@@ -826,6 +818,8 @@ bool pw_check_option(const char *option, const char *value)
 		return global_support.no_color == spa_atob(value);
 	else if (spa_streq(option, "no-config"))
 		return global_support.no_config == spa_atob(value);
+	else if (spa_streq(option, "do-dlclose"))
+		return global_support.do_dlclose == spa_atob(value);
 	return false;
 }
 
diff --git a/src/pipewire/stream.c b/src/pipewire/stream.c
index 808eb47473c460fd659949cf8fda5dc6a5b26415..a7c1da1870c0e92527940bb1792c52f92bfeae87 100644
--- a/src/pipewire/stream.c
+++ b/src/pipewire/stream.c
@@ -324,7 +324,8 @@ static inline int queue_push(struct stream *stream, struct queue *queue, struct
 {
 	uint32_t index;
 
-	if (SPA_FLAG_IS_SET(buffer->flags, BUFFER_FLAG_QUEUED))
+	if (SPA_FLAG_IS_SET(buffer->flags, BUFFER_FLAG_QUEUED) ||
+	    buffer->id >= stream->n_buffers)
 		return -EINVAL;
 
 	SPA_FLAG_SET(buffer->flags, BUFFER_FLAG_QUEUED);
@@ -921,6 +922,9 @@ static int impl_port_use_buffers(void *object,
 	if (impl->disconnecting && n_buffers > 0)
 		return -EIO;
 
+	if (n_buffers > MAX_BUFFERS)
+		return -EINVAL;
+
 	prot = PROT_READ | (direction == SPA_DIRECTION_OUTPUT ? PROT_WRITE : 0);
 
 	clear_buffers(stream);
@@ -956,6 +960,7 @@ static int impl_port_use_buffers(void *object,
 		pw_log_debug("%p: got buffer id:%d datas:%d, mapped size %d", stream, i,
 				buffers[i]->n_datas, size);
 	}
+	impl->n_buffers = n_buffers;
 
 	for (i = 0; i < n_buffers; i++) {
 		struct buffer *b = &impl->buffers[i];
@@ -972,9 +977,6 @@ static int impl_port_use_buffers(void *object,
 
 		pw_stream_emit_add_buffer(stream, &b->this);
 	}
-
-	impl->n_buffers = n_buffers;
-
 	return 0;
 }
 
@@ -1000,6 +1002,7 @@ static int impl_node_process_input(void *object)
 	if (io->status == SPA_STATUS_HAVE_DATA &&
 	    (b = get_buffer(stream, io->buffer_id)) != NULL) {
 		/* push new buffer */
+		pw_log_trace_fp("%p: push %d %p", stream, b->id, io);
 		if (queue_push(impl, &impl->dequeued, b) == 0) {
 			copy_position(impl, impl->dequeued.incount);
 			if (b->busy)
@@ -1007,13 +1010,15 @@ static int impl_node_process_input(void *object)
 			call_process(impl);
 		}
 	}
-	if (io->status != SPA_STATUS_NEED_DATA) {
+	if (io->status != SPA_STATUS_NEED_DATA || io->buffer_id == SPA_ID_INVALID) {
 		/* pop buffer to recycle */
 		if ((b = queue_pop(impl, &impl->queued))) {
 			pw_log_trace_fp("%p: recycle buffer %d", stream, b->id);
-		} else if (io->status == -EPIPE)
-			return io->status;
-		io->buffer_id = b ? b->id : SPA_ID_INVALID;
+			io->buffer_id = b->id;
+		} else {
+			pw_log_trace_fp("%p: no buffers to recycle", stream);
+			io->buffer_id = SPA_ID_INVALID;
+		}
 		io->status = SPA_STATUS_NEED_DATA;
 	}
 	if (impl->driving && impl->using_trigger)
diff --git a/src/pipewire/utils.c b/src/pipewire/utils.c
index 072f472777842b3e25aaee44887b763093766fcb..1d02714f3ba54af83e933f4089fe730e37c81fc7 100644
--- a/src/pipewire/utils.c
+++ b/src/pipewire/utils.c
@@ -97,6 +97,37 @@ char **pw_split_strv(const char *str, const char *delimiter, int max_tokens, int
 	return arr.data;
 }
 
+/** Split a string in-place based on delimiters
+ * \param str a string to split
+ * \param delimiter delimiter characters to split on
+ * \param max_tokens the max number of tokens to split
+ * \param[out] tokens an array to hold up to \a max_tokens of strings
+ * \return the number of tokens in \a tokens
+ *
+ * \a str will be modified in-place so that \a tokens will contain zero terminated
+ * strings split at \a delimiter characters.
+ */
+SPA_EXPORT
+int pw_split_ip(char *str, const char *delimiter, int max_tokens, char *tokens[])
+{
+	const char *state = NULL;
+	char *s, *t;
+	size_t len, l2;
+	int n = 0;
+
+	s = (char *)pw_split_walk(str, delimiter, &len, &state);
+	while (s && n + 1 < max_tokens) {
+		t = (char*)pw_split_walk(str, delimiter, &l2, &state);
+		s[len] = '\0';
+		tokens[n++] = s;
+		s = t;
+		len = l2;
+	}
+	if (s)
+		tokens[n++] = s;
+	return n;
+}
+
 /** Free a NULL terminated array of strings
  * \param str a NULL terminated array of string
  *
diff --git a/src/pipewire/utils.h b/src/pipewire/utils.h
index c04d6ea136fa851edeedef68e2226ede375173dd..8106710dc0009752968a06b6f8cfb3136f1ecb1a 100644
--- a/src/pipewire/utils.h
+++ b/src/pipewire/utils.h
@@ -58,6 +58,9 @@ pw_split_walk(const char *str, const char *delimiter, size_t *len, const char **
 char **
 pw_split_strv(const char *str, const char *delimiter, int max_tokens, int *n_tokens);
 
+int
+pw_split_ip(char *str, const char *delimiter, int max_tokens, char *tokens[]);
+
 void
 pw_free_strv(char **str);
 
diff --git a/src/tools/pw-cli.c b/src/tools/pw-cli.c
index bb5ab99cb521fa6ad2fc5af668ffc39424f4184a..4dfa8c71b5e3e373b4a744876aabbc86b8e7508e 100644
--- a/src/tools/pw-cli.c
+++ b/src/tools/pw-cli.c
@@ -142,26 +142,6 @@ static struct global * obj_global(struct remote_data *rd, uint32_t id);
 static int children_of(struct remote_data *rd, uint32_t parent_id,
 	const char *child_type, uint32_t **children);
 
-static int pw_split_ip(char *str, const char *delimiter, int max_tokens, char *tokens[])
-{
-	const char *state = NULL;
-	char *s, *t;
-	size_t len, l2;
-	int n = 0;
-
-	s = (char *)pw_split_walk(str, delimiter, &len, &state);
-	while (s && n + 1 < max_tokens) {
-		t = (char*)pw_split_walk(str, delimiter, &l2, &state);
-		s[len] = '\0';
-		tokens[n++] = s;
-		s = t;
-		len = l2;
-	}
-	if (s)
-		tokens[n++] = s;
-	return n;
-}
-
 static void print_properties(struct spa_dict *props, char mark, bool header)
 {
 	const struct spa_dict_item *item;
diff --git a/src/tools/pw-mon.c b/src/tools/pw-mon.c
index f7e088c65229b1d2f9ec4c2582c0ae8712a84924..2eeb4dd0e370fe6bad87473f3935f5a492b34ea6 100644
--- a/src/tools/pw-mon.c
+++ b/src/tools/pw-mon.c
@@ -780,7 +780,7 @@ int main(int argc, char *argv[])
 	if (isatty(STDERR_FILENO) && getenv("NO_COLOR") == NULL)
 		colors = true;
 
-	while ((c = getopt_long(argc, argv, "hVr:", long_options, NULL)) != -1) {
+	while ((c = getopt_long(argc, argv, "hVr:NC", long_options, NULL)) != -1) {
 		switch (c) {
 		case 'h':
 			show_help(argv[0], false);
diff --git a/src/tools/pw-top.c b/src/tools/pw-top.c
index 61a05c5958f0ace96a628efc696c9ff3e240171e..7574cab906b41be3aa844f7b87f230631556941a 100644
--- a/src/tools/pw-top.c
+++ b/src/tools/pw-top.c
@@ -663,11 +663,20 @@ static void on_core_error(void *_data, uint32_t id, int seq, int res, const char
 {
 	struct data *data = _data;
 
-	pw_log_error("error id:%u seq:%d res:%d (%s): %s",
-			id, seq, res, spa_strerror(res), message);
-
-	if (id == PW_ID_CORE && res == -EPIPE)
-		pw_main_loop_quit(data->loop);
+	if (id == PW_ID_CORE) {
+		switch (res) {
+		case -EPIPE:
+			pw_main_loop_quit(data->loop);
+			break;
+		default:
+			pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+				id, seq, res, spa_strerror(res), message);
+			break;
+		}
+	} else {
+		pw_log_info("error id:%u seq:%d res:%d (%s): %s",
+				id, seq, res, spa_strerror(res), message);
+	}
 }
 
 static void on_core_done(void *_data, uint32_t id, int seq)
diff --git a/test/test-logger.c b/test/test-logger.c
index 5aca7d1998448472ebc274bfa473517069601136..b40445a3ea6af52a03ef97c156258ade0e0fc8c3 100644
--- a/test/test-logger.c
+++ b/test/test-logger.c
@@ -350,8 +350,10 @@ PWTEST(logger_debug_env_invalid)
 		"invalid value",
 		"*:5,some invalid value",
 		"*:W,foo.bar:3,invalid:",
-		"*:W,1,foo.bar:D",
-		"*:W,D,foo.bar:3",
+		"*:W,2,foo.bar:Q",
+		"*:W,7,foo.bar:D",
+		"*:W,Q,foo.bar:5",
+		"*:W,D,foo.bar:8",
 	};
 
 	pwtest_int_lt(which, SPA_N_ELEMENTS(envvars));
@@ -644,7 +646,7 @@ PWTEST_SUITE(logger)
 		   PWTEST_ARG_RANGE, SPA_LOG_LEVEL_NONE, SPA_LOG_LEVEL_TRACE + 1,
 		   PWTEST_NOARG);
 	pwtest_add(logger_debug_env_invalid,
-		   PWTEST_ARG_RANGE, 0, 5, /* see the test */
+		   PWTEST_ARG_RANGE, 0, 7, /* see the test */
 		   PWTEST_NOARG);
 	pwtest_add(logger_topics, PWTEST_NOARG);
 	pwtest_add(logger_journal, PWTEST_NOARG);
diff --git a/test/test-spa-buffer.c b/test/test-spa-buffer.c
index e9f5620b00fc4aa25de05fed6653462c1a677940..89bfabdcb8dfcf18a28baa6ed96ed6c8ac650ee9 100644
--- a/test/test-spa-buffer.c
+++ b/test/test-spa-buffer.c
@@ -47,7 +47,8 @@ PWTEST(buffer_abi_types)
 	pwtest_int_eq(SPA_META_Cursor, 5);
 	pwtest_int_eq(SPA_META_Control, 6);
 	pwtest_int_eq(SPA_META_Busy, 7);
-	pwtest_int_eq(_SPA_META_LAST, 8);
+	pwtest_int_eq(SPA_META_VideoTransform, 8);
+	pwtest_int_eq(_SPA_META_LAST, 9);
 
 	return PWTEST_PASS;
 }
@@ -64,6 +65,7 @@ PWTEST(buffer_abi_sizes)
 	pwtest_int_eq(sizeof(struct spa_meta_region), 16U);
 	pwtest_int_eq(sizeof(struct spa_meta_bitmap), 20U);
 	pwtest_int_eq(sizeof(struct spa_meta_cursor), 28U);
+	pwtest_int_eq(sizeof(struct spa_meta_videotransform), 4U);
 
 	return PWTEST_PASS;
 #else
@@ -75,6 +77,7 @@ PWTEST(buffer_abi_sizes)
 	fprintf(stderr, "%zd\n", sizeof(struct spa_meta_region));
 	fprintf(stderr, "%zd\n", sizeof(struct spa_meta_bitmap));
 	fprintf(stderr, "%zd\n", sizeof(struct spa_meta_cursor));
+	fprintf(stderr, "%zd\n", sizeof(struct spa_meta_videotransform));
 	return PWTEST_SKIP;
 #endif
 }
diff --git a/test/test-utils.c b/test/test-utils.c
index 9b7c520e244679b43945c8297722f68dd922f011..84a2fcb4cd4fb8b71830fd0ca37e6d0a5fd66d7d 100644
--- a/test/test-utils.c
+++ b/test/test-utils.c
@@ -193,6 +193,8 @@ static void test__pw_split_strv(void)
 {
 	const char *test1 = "a \n test string  \n \r ";
 	const char *del = "\n\r ";
+	const char *test2 = "a:";
+	const char *del2 = ":";
 	int n_tokens;
 	char **res;
 
@@ -212,6 +214,13 @@ static void test__pw_split_strv(void)
 	pwtest_str_eq(res[1], "test string  \n \r ");
 	pwtest_ptr_null(res[2]);
 	pw_free_strv(res);
+
+	res = pw_split_strv(test2, del2, 2, &n_tokens);
+	pwtest_ptr_notnull(res);
+	pwtest_int_eq(n_tokens, 1);
+	pwtest_str_eq(res[0], "a");
+	pwtest_ptr_null(res[1]);
+	pw_free_strv(res);
 }
 
 PWTEST(utils_split)