diff --git a/meta-multimedia/recipes-multimedia/pipewire/pipewire/0001-pipewire-compress-offload.patch b/meta-multimedia/recipes-multimedia/pipewire/pipewire/0001-pipewire-compress-offload.patch
index 8a884d64cd..389893fb1c 100644
--- a/meta-multimedia/recipes-multimedia/pipewire/pipewire/0001-pipewire-compress-offload.patch
+++ b/meta-multimedia/recipes-multimedia/pipewire/pipewire/0001-pipewire-compress-offload.patch
@@ -1,42 +1,345 @@
-From 345e4b033c2ff69ab7aa02a123f6041b878f7fb9 Mon Sep 17 00:00:00 2001
+From 867d36a330d78e4e55348983eb0701c5d3cd7fa1 Mon Sep 17 00:00:00 2001
 From: Harsh Rai <harsh.rai@oss.qualcomm.com>
-Date: Tue, 16 Jun 2026 19:21:37 +0530
-Subject: [PATCH] pulse: enable MP3 compressed offload via Pulse
+Date: Fri, 23 Oct 2026 17:22:01 +0530
+Subject: [PATCH 1/4] spa: expose the AAC audio object type
 
-Add PipeWire Pulse virtual sink and forwarder for offload.
-Route compressed Pulse streams to PAL compressed sink.
+Add an audio object type field to spa_audio_info_aac and define the
+matching SPA format property.
+
+Parse the property when an AAC format pod is received and include it
+when building an AAC format pod. Omit the property when its value is
+zero so existing AAC format descriptions remain unchanged when no
+object type is available.
+
+The audio object type identifies the MPEG-4 AAC profile independently
+of the transport format, allowing raw AAC and ADTS streams to carry the
+profile selected by the client.
 
 Upstream-Status: Submitted [https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/2878]
+Signed-off-by: Harsh Rai <harsh.rai@oss.qualcomm.com>
+---
+ spa/include/spa/param/audio/aac-utils.h | 6 +++++-
+ spa/include/spa/param/audio/aac.h       | 1 +
+ spa/include/spa/param/format-types.h    | 2 ++
+ spa/include/spa/param/format.h          | 2 ++
+ 4 files changed, 10 insertions(+), 1 deletion(-)
+
+diff --git a/spa/include/spa/param/audio/aac-utils.h b/spa/include/spa/param/audio/aac-utils.h
+index 3ec362d19..7be3a7c94 100644
+--- a/spa/include/spa/param/audio/aac-utils.h
++++ b/spa/include/spa/param/audio/aac-utils.h
+@@ -36,7 +36,8 @@ spa_format_audio_aac_parse(const struct spa_pod *format, struct spa_audio_info_a
+ 			SPA_FORMAT_AUDIO_rate,		SPA_POD_OPT_Int(&info->rate),
+ 			SPA_FORMAT_AUDIO_channels,	SPA_POD_OPT_Int(&info->channels),
+ 			SPA_FORMAT_AUDIO_bitrate,	SPA_POD_OPT_Int(&info->bitrate),
+-			SPA_FORMAT_AUDIO_AAC_streamFormat,	SPA_POD_OPT_Id(&info->stream_format));
++			SPA_FORMAT_AUDIO_AAC_streamFormat,	SPA_POD_OPT_Id(&info->stream_format),
++			SPA_FORMAT_AUDIO_AAC_audioObjectType,	SPA_POD_OPT_Int(&info->audio_object_type));
+ 
+ 	return res;
+ }
+@@ -64,6 +65,9 @@ spa_format_audio_aac_build(struct spa_pod_builder *builder, uint32_t id,
+ 	if (info->stream_format != 0)
+ 		spa_pod_builder_add(builder,
+ 			SPA_FORMAT_AUDIO_AAC_streamFormat,	SPA_POD_Id(info->stream_format), 0);
++	if (info->audio_object_type != 0)
++		spa_pod_builder_add(builder,
++			SPA_FORMAT_AUDIO_AAC_audioObjectType,	SPA_POD_Int(info->audio_object_type), 0);
+ 	return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+ }
+ 
+diff --git a/spa/include/spa/param/audio/aac.h b/spa/include/spa/param/audio/aac.h
+index 1a7fb1612..db6ca083a 100644
+--- a/spa/include/spa/param/audio/aac.h
++++ b/spa/include/spa/param/audio/aac.h
+@@ -41,6 +41,7 @@ struct spa_audio_info_aac {
+ 	uint32_t channels;				/*< number of channels */
+ 	uint32_t bitrate;				/*< stream bitrate */
+ 	enum spa_audio_aac_stream_format stream_format;	/*< AAC audio stream format */
++	uint32_t audio_object_type;			/*< MPEG-4 audio object type */
+ };
+ 
+ #define SPA_AUDIO_INFO_AAC_INIT(...)		((struct spa_audio_info_aac) { __VA_ARGS__ })
+diff --git a/spa/include/spa/param/format-types.h b/spa/include/spa/param/format-types.h
+index 1ca4f729b..99f60fcb7 100644
+--- a/spa/include/spa/param/format-types.h
++++ b/spa/include/spa/param/format-types.h
+@@ -144,6 +144,8 @@ static const struct spa_type_info spa_type_format[] = {
+ 
+ 	{ SPA_FORMAT_AUDIO_AAC_streamFormat, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_AAC_BASE "streamFormat",
+ 		spa_type_audio_aac_stream_format },
++	{ SPA_FORMAT_AUDIO_AAC_audioObjectType, SPA_TYPE_Int,
++		SPA_TYPE_INFO_FORMAT_AUDIO_AAC_BASE "audioObjectType", NULL },
+ 	{ SPA_FORMAT_AUDIO_WMA_profile, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_WMA_BASE "profile",
+ 		spa_type_audio_wma_profile },
+ 	{ SPA_FORMAT_AUDIO_AMR_bandMode, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_AMR_BASE "bandMode",
+diff --git a/spa/include/spa/param/format.h b/spa/include/spa/param/format.h
+index bef5116e9..d5e0f00c7 100644
+--- a/spa/include/spa/param/format.h
++++ b/spa/include/spa/param/format.h
+@@ -110,6 +110,8 @@ enum spa_format {
+ 	SPA_FORMAT_AUDIO_blockAlign,    	/**< audio data block alignment (Int) */
+ 
+ 	SPA_FORMAT_AUDIO_AAC_streamFormat,	/**< AAC stream format, (Id enum spa_audio_aac_stream_format) */
++	SPA_FORMAT_AUDIO_AAC_audioObjectType = SPA_FORMAT_START_Audio + 0x100,
++						/**< AAC audio object type (Int) */
+ 
+ 	SPA_FORMAT_AUDIO_WMA_profile,		/**< WMA profile (Id enum spa_audio_wma_profile) */
+ 
+-- 
+GitLab
+
+
+From a009b6ba406ce7f12c4cda8ab52d6fb18a8db29a Mon Sep 17 00:00:00 2001
+From: Harsh Rai <harsh.rai@oss.qualcomm.com>
+Date: Fri, 23 Oct 2026 17:22:01 +0530
+Subject: [PATCH 2/4] pulse-server: describe MP3 and AAC encoded formats
 
+Add Pulse format encodings for MP3, AAC with ADTS transport, and raw
+AAC.
+
+Convert PipeWire MP3 and AAC format pods into Pulse format information,
+including sample rate, channel count, bitrate, AAC transport format,
+and AAC audio object type. Build the corresponding PipeWire format pods
+when a Pulse client selects one of these encoded formats.
+
+This keeps encoded stream negotiation in the existing Pulse format
+conversion layer and preserves codec parameters without treating the
+payload as PCM audio.
+
+Upstream-Status: Submitted [https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/2878]
 Signed-off-by: Harsh Rai <harsh.rai@oss.qualcomm.com>
 ---
- src/modules/meson.build                       |   2 +
- .../module-compress-offload-forwarder.c       | 649 ++++++++++++++++++
- .../modules/module-compress-offload-sink.c    | 293 ++++++++
- .../module-protocol-pulse/pulse-server.c      | 254 ++++++-
- 4 files changed, 1187 insertions(+), 11 deletions(-)
- create mode 100644 src/modules/module-protocol-pulse/modules/module-compress-offload-forwarder.c
- create mode 100644 src/modules/module-protocol-pulse/modules/module-compress-offload-sink.c
+ src/modules/module-protocol-pulse/format.c | 115 +++++++++++++++++++++
+ src/modules/module-protocol-pulse/format.h |   3 +
+ 2 files changed, 118 insertions(+)
+
+diff --git a/src/modules/module-protocol-pulse/format.c b/src/modules/module-protocol-pulse/format.c
+index 913cfe996..d16f824d0 100644
+--- a/src/modules/module-protocol-pulse/format.c
++++ b/src/modules/module-protocol-pulse/format.c
+@@ -484,6 +484,9 @@ static const struct encoding_info encoding_names[] = {
+ 	[ENCODING_MPEG2_AAC_IEC61937] = { "MPEG2-AAC-IEC61937", SPA_AUDIO_IEC958_CODEC_MPEG2_AAC },
+ 	[ENCODING_TRUEHD_IEC61937] = { "TRUEHD-IEC61937", SPA_AUDIO_IEC958_CODEC_TRUEHD },
+ 	[ENCODING_DTSHD_IEC61937] = { "DTSHD-IEC61937", SPA_AUDIO_IEC958_CODEC_DTSHD },
++	[ENCODING_MP3] = { "MP3", SPA_ID_INVALID },
++	[ENCODING_AAC] = { "AAC", SPA_ID_INVALID },
++	[ENCODING_AAC_ADTS] = { "AAC-ADTS", SPA_ID_INVALID },
+ };
+ 
+ const char *format_encoding2name(enum encoding enc)
+@@ -775,6 +778,39 @@ static int format_info_iec958_from_param(struct format_info *info, struct spa_po
+ 	return add_int(info, "format.rate", param, SPA_FORMAT_AUDIO_rate);
+ }
+ 
++static int format_info_encoded_from_param(struct format_info *info, struct spa_pod *param,
++		uint32_t index, enum encoding encoding)
++{
++	struct spa_audio_info audio_info;
++
++	if (index > 0)
++		return -ENOENT;
++	if (spa_format_audio_parse(param, &audio_info) < 0)
++		return -ENOTSUP;
++	if ((info->props = pw_properties_new(NULL, NULL)) == NULL)
++		return -errno;
++	info->encoding = encoding;
++
++	switch (encoding) {
++	case ENCODING_MP3:
++		pw_properties_setf(info->props, "format.rate", "%u", audio_info.info.mp3.rate);
++		pw_properties_setf(info->props, "format.channels", "%u", audio_info.info.mp3.channels);
++		break;
++	case ENCODING_AAC:
++	case ENCODING_AAC_ADTS:
++		pw_properties_setf(info->props, "format.rate", "%u", audio_info.info.aac.rate);
++		pw_properties_setf(info->props, "format.channels", "%u", audio_info.info.aac.channels);
++		pw_properties_setf(info->props, "format.bit_rate", "%u", audio_info.info.aac.bitrate);
++		pw_properties_set(info->props, "format.stream_format",
++				audio_info.info.aac.stream_format == SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS ?
++				"\"adts\"" : "\"raw\"");
++		break;
++	default:
++		return -ENOTSUP;
++	}
++	return 0;
++}
++
+ int format_info_from_param(struct format_info *info, struct spa_pod *param, uint32_t index)
+ {
+ 	uint32_t media_type, media_subtype;
+@@ -793,6 +829,20 @@ int format_info_from_param(struct format_info *info, struct spa_pod *param, uint
+ 	case SPA_MEDIA_SUBTYPE_iec958:
+ 		res = format_info_iec958_from_param(info, param, index);
+ 		break;
++	case SPA_MEDIA_SUBTYPE_mp3:
++		res = format_info_encoded_from_param(info, param, index, ENCODING_MP3);
++		break;
++	case SPA_MEDIA_SUBTYPE_aac:
++	{
++		struct spa_audio_info audio_info;
++
++		if (spa_format_audio_parse(param, &audio_info) < 0)
++			return -ENOTSUP;
++		res = format_info_encoded_from_param(info, param, index,
++				audio_info.info.aac.stream_format == SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS ?
++				ENCODING_AAC_ADTS : ENCODING_AAC);
++		break;
++	}
+ 	default:
+ 		return -ENOTSUP;
+ 	}
+@@ -836,6 +886,36 @@ static int format_info_get_rate(const struct format_info *info)
+ 	return -ENOTSUP;
+ }
+ 
++static uint32_t format_info_get_u32_prop(const struct format_info *info,
++		const char *key, uint32_t default_value)
++{
++	const char *str, *val;
++	struct spa_json it[2];
++	int len, value;
++
++	if (info->props == NULL || (str = pw_properties_get(info->props, key)) == NULL)
++		return default_value;
++	if ((len = spa_json_begin(&it[0], str, strlen(str), &val)) <= 0)
++		return default_value;
++	if (spa_json_is_int(val, len) && spa_json_parse_int(val, len, &value) > 0 && value > 0)
++		return (uint32_t)value;
++	return default_value;
++}
++
++static bool format_info_string_prop_equals(const struct format_info *info,
++		const char *key, const char *value)
++{
++	const char *str, *val;
++	struct spa_json it[2];
++	int len;
++
++	if (info->props == NULL || (str = pw_properties_get(info->props, key)) == NULL)
++		return false;
++	if ((len = spa_json_begin(&it[0], str, strlen(str), &val)) > 0 && spa_json_is_string(val, len))
++		return strlen(value) == (size_t)len - 2 && strncasecmp(val + 1, value, len - 2) == 0;
++	return spa_streq(str, value);
++}
++
+ int format_info_to_spec(const struct format_info *info, struct sample_spec *ss,
+ 			  struct channel_map *map)
+ {
+@@ -926,6 +1006,41 @@ const struct spa_pod *format_info_build_param(struct spa_pod_builder *b, uint32_
+ 		param = spa_format_audio_iec958_build(b, id, &i);
+ 		break;
+ 	}
++	case ENCODING_MP3:
++	case ENCODING_AAC:
++	case ENCODING_AAC_ADTS:
++	{
++		struct spa_audio_info audio_info;
++		uint32_t channels;
++
++		spa_zero(audio_info);
++		audio_info.media_type = SPA_MEDIA_TYPE_audio;
++		if ((res = format_info_get_rate(info)) <= 0) {
++			errno = -res;
++			return NULL;
++		}
++		channels = format_info_get_u32_prop(info, "format.channels", 2);
++		if (info->encoding == ENCODING_MP3) {
++			audio_info.media_subtype = SPA_MEDIA_SUBTYPE_mp3;
++			audio_info.info.mp3.rate = res;
++			audio_info.info.mp3.channels = channels;
++			audio_info.info.mp3.channel_mode = channels == 1 ?
++				SPA_AUDIO_MP3_CHANNEL_MODE_MONO : SPA_AUDIO_MP3_CHANNEL_MODE_STEREO;
++		} else {
++			audio_info.media_subtype = SPA_MEDIA_SUBTYPE_aac;
++			audio_info.info.aac.rate = res;
++			audio_info.info.aac.channels = channels;
++			audio_info.info.aac.bitrate = format_info_get_u32_prop(info, "format.bit_rate", 128000);
++			audio_info.info.aac.stream_format =
++					info->encoding == ENCODING_AAC_ADTS ||
++					format_info_string_prop_equals(info, "format.stream_format", "adts") ?
++					SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS :
++					SPA_AUDIO_AAC_STREAM_FORMAT_RAW;
++		}
++		*rate = res;
++		param = spa_format_audio_build(b, id, &audio_info);
++		break;
++	}
+ 	default:
+ 		errno = ENOTSUP;
+ 		break;
+diff --git a/src/modules/module-protocol-pulse/format.h b/src/modules/module-protocol-pulse/format.h
+index c91ec7076..d873a8065 100644
+--- a/src/modules/module-protocol-pulse/format.h
++++ b/src/modules/module-protocol-pulse/format.h
+@@ -163,6 +163,9 @@ enum encoding {
+ 	ENCODING_MPEG2_AAC_IEC61937,
+ 	ENCODING_TRUEHD_IEC61937,
+ 	ENCODING_DTSHD_IEC61937,
++	ENCODING_MP3,
++	ENCODING_AAC,
++	ENCODING_AAC_ADTS,
+ 	ENCODING_MAX,
+ 	ENCODING_INVALID = -1,
+ };
+-- 
+GitLab
+
+
+From a06592eb77ded07fe3673fd1a1098304f1995a35 Mon Sep 17 00:00:00 2001
+From: Harsh Rai <harsh.rai@oss.qualcomm.com>
+Date: Fri, 23 Oct 2026 17:45:41 +0530
+Subject: [PATCH 3/4] pulse-server: add compressed-offload sink modules
+
+Add a Pulse-visible sink module for compressed playback and a forwarder
+module that sends its encoded stream to a configured compressed-offload
+sink.
+
+The sink module creates the Pulse endpoint and carries codec properties
+for MP3, AAC ADTS, and raw AAC streams. The forwarder observes the
+encoded monitor stream, preserves packet data, creates a matching target
+stream, and forwards codec metadata without decoding or converting the
+payload.
+
+Handle target discovery, stream creation, pending data, client removal,
+target removal, state transitions, and module cleanup so the forwarding
+stream is destroyed when either side disappears.
+
+Register both modules in the protocol-pulse build and keep their module
+arguments and descriptions independent of any particular hardware or
+backend implementation.
+
+Upstream-Status: Submitted [https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/2878]
+Signed-off-by: Harsh Rai <harsh.rai@oss.qualcomm.com>
+---
+ src/modules/meson.build                       |    2 +
+ .../pulse-module-compress-offload-forwarder.c | 1052 +++++++++++++++++
+ .../pulse-module-compress-offload-forwarder.h |   20 +
+ .../pulse-module-compress-offload-sink.c      |  308 +++++
+ 4 files changed, 1382 insertions(+)
+ create mode 100644 src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-forwarder.c
+ create mode 100644 src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-forwarder.h
+ create mode 100644 src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-sink.c
 
 diff --git a/src/modules/meson.build b/src/modules/meson.build
-index 59f46ae..900871d 100644
+index 79f599ba2..5802ab1dd 100644
 --- a/src/modules/meson.build
 +++ b/src/modules/meson.build
-@@ -324,6 +324,8 @@ pipewire_module_protocol_pulse_sources = [
-   'module-protocol-pulse/modules/module-alsa-source.c',
-   'module-protocol-pulse/modules/module-always-sink.c',
-   'module-protocol-pulse/modules/module-combine-sink.c',
-+  'module-protocol-pulse/modules/module-compress-offload-sink.c',
-+  'module-protocol-pulse/modules/module-compress-offload-forwarder.c',
-   'module-protocol-pulse/modules/module-device-manager.c',
-   'module-protocol-pulse/modules/module-device-restore.c',
-   'module-protocol-pulse/modules/module-echo-cancel.c',
-diff --git a/src/modules/module-protocol-pulse/modules/module-compress-offload-forwarder.c b/src/modules/module-protocol-pulse/modules/module-compress-offload-forwarder.c
+@@ -338,6 +338,8 @@ pipewire_module_protocol_pulse_sources = [
+   'module-protocol-pulse/modules/pulse-module-alsa-source.c',
+   'module-protocol-pulse/modules/pulse-module-always-sink.c',
+   'module-protocol-pulse/modules/pulse-module-combine-sink.c',
++  'module-protocol-pulse/modules/pulse-module-compress-offload-forwarder.c',
++  'module-protocol-pulse/modules/pulse-module-compress-offload-sink.c',
+   'module-protocol-pulse/modules/pulse-module-device-manager.c',
+   'module-protocol-pulse/modules/pulse-module-device-restore.c',
+   'module-protocol-pulse/modules/pulse-module-echo-cancel.c',
+diff --git a/src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-forwarder.c b/src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-forwarder.c
 new file mode 100644
-index 0000000..33054e4
+index 000000000..8afc1c425
 --- /dev/null
-+++ b/src/modules/module-protocol-pulse/modules/module-compress-offload-forwarder.c
-@@ -0,0 +1,649 @@
++++ b/src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-forwarder.c
+@@ -0,0 +1,1052 @@
 +/* PipeWire */
 +/* Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */
 +/* SPDX-License-Identifier: BSD-3-Clause-Clear */
@@ -55,19 +358,32 @@ index 0000000..33054e4
 +#include <pipewire/pipewire.h>
 +
 +#include "../module.h"
++#include "pulse-module-compress-offload-forwarder.h"
++
++/** \page page_pulse_module_compress_offload_forwarder Compress Offload Forwarder
++ *
++ * ## Module Name
++ *
++ * `module-compress-offload-forwarder`
++ *
++ * ## Module Options
++ *
++ * @pulse_module_options@
++ */
 +
 +#define NAME "compress-offload-forwarder"
 +
-+#define DEFAULT_CODEC "mp3"
++#define DEFAULT_CODEC "auto"
 +#define DEFAULT_RATE 48000u
 +#define DEFAULT_CHANNELS 2u
 +#define DEFAULT_BITRATE 128000u
 +#define DEFAULT_BLOCK_SIZE (16u * 1024u)
++#define MAX_PENDING_SIZE (1024u * 1024u)
 +
-+static const char *const forwarder_options =
++static const char *const pulse_module_options =
 +	"source_sink=<Pulse-visible compress sink node.name> "
-+	"target_sink=<PAL compress sink node.name> "
-+	"codec=<codec name, default mp3> "
++	"target_sink=<compressed-offload sink node.name> "
++	"codec=<codec name, default auto> "
 +	"rate=<sample rate, default 48000> "
 +	"channels=<number of channels, default 2> "
 +	"bitrate=<bit rate, default 128000> "
@@ -83,6 +399,8 @@ index 0000000..33054e4
 +	struct spa_hook core_listener;
 +	struct pw_registry *registry;
 +	struct spa_hook registry_listener;
++	struct pw_node *client_node;
++	struct spa_hook client_node_listener;
 +
 +	struct pw_stream *capture;
 +	struct spa_hook capture_listener;
@@ -93,11 +411,15 @@ index 0000000..33054e4
 +	char monitor_name[280];
 +	char target_name[256];
 +	char codec[32];
++	char aac_stream_format[16];
++	char aac_codec_data[256];
 +	uint32_t rate;
 +	uint32_t channels;
 +	uint32_t bitrate;
++	uint32_t aac_audio_object_type;
 +	uint32_t block_size;
 +	uint32_t target_id;
++	uint32_t client_node_id;
 +	uint8_t *pending_data;
 +	uint32_t pending_size;
 +	uint32_t pending_offset;
@@ -108,12 +430,37 @@ index 0000000..33054e4
 +	uint8_t activate_pending:1;
 +	uint8_t playback_active:1;
 +	uint8_t playback_failed:1;
++	uint8_t metadata_error_logged:1;
 +};
 +
++static struct forwarder_data *active_forwarder;
++
++static void destroy_playback_stream(struct forwarder_data *d);
++
 +static int create_playback_stream(struct forwarder_data *d);
 +static void set_playback_active(struct forwarder_data *d, bool active);
 +static void maybe_create_playback_stream(struct forwarder_data *d, const char *reason);
 +
++static bool codec_is_auto(const char *codec)
++{
++	return codec == NULL || codec[0] == '\0' || spa_streq(codec, "auto");
++}
++
++static void reset_codec_state(struct forwarder_data *d)
++{
++	spa_scnprintf(d->codec, sizeof(d->codec), "%s", DEFAULT_CODEC);
++	d->aac_stream_format[0] = '\0';
++	d->aac_codec_data[0] = '\0';
++	d->aac_audio_object_type = 0;
++	d->rate = DEFAULT_RATE;
++	d->channels = DEFAULT_CHANNELS;
++	d->bitrate = DEFAULT_BITRATE;
++	d->logged_first_buffer = false;
++	d->logged_first_output_buffer = false;
++	d->logged_silence_buffer = false;
++	d->metadata_error_logged = false;
++}
++
 +static void request_playback_activation(struct forwarder_data *d)
 +{
 +	if (d->activate_event == NULL || d->activate_pending)
@@ -173,6 +520,8 @@ index 0000000..33054e4
 +	if (size == 0)
 +		return 0;
 +	compact_pending_data(d);
++	if (size > MAX_PENDING_SIZE || d->pending_size > MAX_PENDING_SIZE - size)
++		return -ENOSPC;
 +	if (d->pending_size > UINT32_MAX - size)
 +		return -EOVERFLOW;
 +	pending_data = realloc(d->pending_data, d->pending_size + size);
@@ -184,6 +533,205 @@ index 0000000..33054e4
 +	return 0;
 +}
 +
++static bool codec_is_mp3(const char *codec)
++{
++	return codec != NULL && spa_streq(codec, "mp3");
++}
++
++static bool codec_is_aac_adts(const char *codec)
++{
++	return codec != NULL && spa_streq(codec, "aac_adts");
++}
++
++static bool codec_is_aac_raw(const char *codec)
++{
++	return codec != NULL && spa_streq(codec, "aac");
++}
++
++static bool codec_is_aac(const char *codec)
++{
++	return codec_is_aac_adts(codec) || codec_is_aac_raw(codec);
++}
++
++static bool raw_aac_transport_header(const uint8_t *data, uint32_t size,
++		uint32_t *frame_size)
++{
++	if (size < COMPRESS_RAW_AAC_HEADER_SIZE ||
++	    data[0] != COMPRESS_RAW_AAC_MAGIC_0 ||
++	    data[1] != COMPRESS_RAW_AAC_MAGIC_1 ||
++	    data[2] != COMPRESS_RAW_AAC_MAGIC_2 ||
++	    data[3] != COMPRESS_RAW_AAC_MAGIC_3)
++		return false;
++	*frame_size = (uint32_t)data[4] |
++			((uint32_t)data[5] << 8) |
++			((uint32_t)data[6] << 16) |
++			((uint32_t)data[7] << 24);
++	return *frame_size > 0 && *frame_size <= MAX_PENDING_SIZE;
++}
++
++static bool payload_is_aac_adts(const uint8_t *bytes, uint32_t size)
++{
++	return size >= 2 &&
++		bytes[0] == 0xff &&
++		(bytes[1] & 0xf6) == 0xf0;
++}
++
++static bool parse_aac_adts_header(const uint8_t *bytes, uint32_t size,
++		uint32_t *rate, uint32_t *channels, uint32_t *profile)
++{
++	static const uint32_t sample_rates[] = {
++		96000, 88200, 64000, 48000, 44100, 32000,
++		24000, 22050, 16000, 12000, 11025, 8000,
++		7350,
++	};
++	uint32_t sampling_index, parsed_channels, parsed_profile;
++
++	if (size < 7 || !payload_is_aac_adts(bytes, size))
++		return false;
++
++	parsed_profile = ((bytes[2] & 0xc0) >> 6) + 1;
++	sampling_index = (bytes[2] & 0x3c) >> 2;
++	parsed_channels = ((bytes[2] & 0x01) << 2) | ((bytes[3] & 0xc0) >> 6);
++
++	if (sampling_index >= SPA_N_ELEMENTS(sample_rates) || parsed_channels == 0 || parsed_channels > 8)
++		return false;
++
++	if (rate != NULL)
++		*rate = sample_rates[sampling_index];
++	if (channels != NULL)
++		*channels = parsed_channels;
++	if (profile != NULL)
++		*profile = parsed_profile;
++	return true;
++}
++
++static bool payload_is_mp3(const uint8_t *bytes, uint32_t size)
++{
++	return (size >= 3 && bytes[0] == 'I' && bytes[1] == 'D' && bytes[2] == '3') ||
++		(size >= 2 &&
++		 bytes[0] == 0xff &&
++		 (bytes[1] & 0xe0) == 0xe0 &&
++		 (bytes[1] & 0x06) != 0x00);
++}
++
++static bool detect_payload_codec(struct forwarder_data *d, const void *data, uint32_t size)
++{
++	const uint8_t *bytes = data;
++	uint32_t rate, channels, profile;
++
++	if (payload_is_aac_adts(bytes, size)) {
++		if (!parse_aac_adts_header(bytes, size, &rate, &channels, &profile)) {
++			pw_log_warn("compress-forwarder rejected invalid AAC ADTS header=%02x %02x %02x %02x size=%u",
++					size > 0 ? bytes[0] : 0, size > 1 ? bytes[1] : 0,
++					size > 2 ? bytes[2] : 0, size > 3 ? bytes[3] : 0, size);
++			return false;
++		}
++		if (!codec_is_aac_adts(d->codec)) {
++			spa_scnprintf(d->codec, sizeof(d->codec), "aac_adts");
++		}
++		d->rate = rate;
++		d->channels = channels;
++		d->aac_audio_object_type = profile;
++		pw_log_debug("compress-forwarder detected AAC ADTS payload header=%02x %02x rate=%u channels=%u object_type=%u",
++				bytes[0], bytes[1], d->rate, d->channels, d->aac_audio_object_type);
++		return true;
++	}
++	if (payload_is_mp3(bytes, size)) {
++		if (!codec_is_mp3(d->codec)) {
++			spa_scnprintf(d->codec, sizeof(d->codec), "mp3");
++			pw_log_debug("compress-forwarder detected MP3 payload header=%02x %02x %02x",
++					size > 0 ? bytes[0] : 0, size > 1 ? bytes[1] : 0, size > 2 ? bytes[2] : 0);
++		}
++		return true;
++	}
++	return codec_is_aac_raw(d->codec);
++}
++
++static bool update_codec_from_props(struct forwarder_data *d, const struct spa_dict *props)
++{
++	const char *codec, *str;
++	char old_codec[sizeof(d->codec)];
++	char old_aac_stream_format[sizeof(d->aac_stream_format)];
++	char old_aac_codec_data[sizeof(d->aac_codec_data)];
++	uint32_t old_rate, old_channels, old_bitrate, old_aac_audio_object_type;
++	bool changed;
++
++	codec = spa_dict_lookup(props, "codec.type");
++	if (codec == NULL || (!codec_is_mp3(codec) && !codec_is_aac(codec)))
++		return false;
++
++	spa_scnprintf(old_codec, sizeof(old_codec), "%s", d->codec);
++	spa_scnprintf(old_aac_stream_format, sizeof(old_aac_stream_format), "%s", d->aac_stream_format);
++	spa_scnprintf(old_aac_codec_data, sizeof(old_aac_codec_data), "%s", d->aac_codec_data);
++	old_rate = d->rate;
++	old_channels = d->channels;
++	old_bitrate = d->bitrate;
++	old_aac_audio_object_type = d->aac_audio_object_type;
++
++	spa_scnprintf(d->codec, sizeof(d->codec), "%s", codec);
++	if ((str = spa_dict_lookup(props, "codec.sample_rate")) != NULL)
++		d->rate = (uint32_t)atoi(str);
++	if ((str = spa_dict_lookup(props, "codec.channels")) != NULL)
++		d->channels = (uint32_t)atoi(str);
++	if ((str = spa_dict_lookup(props, "codec.bit_rate")) != NULL)
++		d->bitrate = (uint32_t)atoi(str);
++	if ((str = spa_dict_lookup(props, "codec.aac.stream_format")) != NULL)
++		spa_scnprintf(d->aac_stream_format, sizeof(d->aac_stream_format), "%s", str);
++	if ((str = spa_dict_lookup(props, "codec.aac.codec_data")) != NULL)
++		spa_scnprintf(d->aac_codec_data, sizeof(d->aac_codec_data), "%s", str);
++	if ((str = spa_dict_lookup(props, "codec.aac.audio_object_type")) != NULL)
++		d->aac_audio_object_type = (uint32_t)atoi(str);
++	if (codec_is_aac_adts(d->codec) && d->aac_stream_format[0] == '\0')
++		spa_scnprintf(d->aac_stream_format, sizeof(d->aac_stream_format), "adts");
++	if (codec_is_aac_raw(d->codec) && d->aac_stream_format[0] == '\0')
++		spa_scnprintf(d->aac_stream_format, sizeof(d->aac_stream_format), "raw");
++	pw_log_debug("compress-forwarder stream codec update codec=%s rate=%u channels=%u bitrate=%u stream_format=%s object_type=%u",
++			d->codec, d->rate, d->channels, d->bitrate,
++			d->aac_stream_format[0] != '\0' ? d->aac_stream_format : "none",
++			d->aac_audio_object_type);
++
++	changed = !spa_streq(old_codec, d->codec) ||
++			old_rate != d->rate ||
++			old_channels != d->channels ||
++			old_bitrate != d->bitrate ||
++			!spa_streq(old_aac_stream_format, d->aac_stream_format) ||
++			!spa_streq(old_aac_codec_data, d->aac_codec_data) ||
++			old_aac_audio_object_type != d->aac_audio_object_type;
++	return changed;
++}
++
++void compress_offload_forwarder_update(const char *source_name,
++		const struct spa_dict *props)
++{
++	struct forwarder_data *d = active_forwarder;
++
++	if (d == NULL || props == NULL || !spa_streq(source_name, d->source_name))
++		return;
++	if (!update_codec_from_props(d, props))
++		return;
++
++	pw_log_debug("compress-forwarder direct stream codec update codec=%s rate=%u channels=%u object_type=%u",
++			d->codec, d->rate, d->channels, d->aac_audio_object_type);
++	if (d->playback != NULL)
++		destroy_playback_stream(d);
++	if (d->capture_streaming && d->pending_size > d->pending_offset)
++		request_playback_activation(d);
++}
++
++void compress_offload_forwarder_teardown(const char *source_name)
++{
++	struct forwarder_data *d = active_forwarder;
++
++	if (d == NULL || source_name == NULL ||
++	    !spa_streq(source_name, d->source_name))
++		return;
++
++	pw_log_debug("compress-forwarder source:%s playback stream ended, destroying target stream",
++			d->source_name);
++	destroy_playback_stream(d);
++	clear_pending_data(d);
++}
++
 +static void maybe_create_playback_stream(struct forwarder_data *d, const char *reason)
 +{
 +	int res;
@@ -245,11 +793,27 @@ index 0000000..33054e4
 +
 +	spa_zero(info);
 +	info.media_type = SPA_MEDIA_TYPE_audio;
-+	info.media_subtype = SPA_MEDIA_SUBTYPE_mp3;
-+	info.info.mp3.rate = d->rate;
-+	info.info.mp3.channels = d->channels;
-+	info.info.mp3.channel_mode = d->channels == 1 ?
-+		SPA_AUDIO_MP3_CHANNEL_MODE_MONO : SPA_AUDIO_MP3_CHANNEL_MODE_STEREO;
++	if (codec_is_aac_adts(d->codec)) {
++		info.media_subtype = SPA_MEDIA_SUBTYPE_aac;
++		info.info.aac.rate = d->rate;
++		info.info.aac.channels = d->channels;
++		info.info.aac.bitrate = d->bitrate;
++		info.info.aac.stream_format = SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS;
++		info.info.aac.audio_object_type = d->aac_audio_object_type;
++	} else if (codec_is_aac_raw(d->codec)) {
++		info.media_subtype = SPA_MEDIA_SUBTYPE_aac;
++		info.info.aac.rate = d->rate;
++		info.info.aac.channels = d->channels;
++		info.info.aac.bitrate = d->bitrate;
++		info.info.aac.stream_format = SPA_AUDIO_AAC_STREAM_FORMAT_RAW;
++		info.info.aac.audio_object_type = d->aac_audio_object_type;
++	} else {
++		info.media_subtype = SPA_MEDIA_SUBTYPE_mp3;
++		info.info.mp3.rate = d->rate;
++		info.info.mp3.channels = d->channels;
++		info.info.mp3.channel_mode = d->channels == 1 ?
++			SPA_AUDIO_MP3_CHANNEL_MODE_MONO : SPA_AUDIO_MP3_CHANNEL_MODE_STEREO;
++	}
 +
 +	return spa_format_audio_build(builder, id, &info);
 +}
@@ -284,8 +848,15 @@ index 0000000..33054e4
 +	if (d->playback_failed)
 +		destroy_playback_stream(d);
 +	d->capture_streaming = state == PW_STREAM_STATE_STREAMING;
-+	if (state == PW_STREAM_STATE_PAUSED || state == PW_STREAM_STATE_ERROR)
-+		set_playback_active(d, false);
++	if (state == PW_STREAM_STATE_PAUSED || state == PW_STREAM_STATE_ERROR ||
++	    state == PW_STREAM_STATE_UNCONNECTED) {
++		if (d->playback != NULL) {
++			pw_log_debug("compress-forwarder source:%s stopped, destroying target stream",
++					d->monitor_name);
++			destroy_playback_stream(d);
++		}
++		clear_pending_data(d);
++	}
 +}
 +
 +static void capture_process(void *data)
@@ -294,9 +865,11 @@ index 0000000..33054e4
 +	struct pw_buffer *in_buf, *out_buf;
 +	struct spa_data *in_data, *out_data;
 +	uint32_t in_offset, in_size, out_size, copy_size;
++	uint32_t raw_frame_size = 0;
 +	const void *input_data;
 +	const void *copy_data;
 +	uint32_t copy_available;
++	bool framed_raw_aac = false;
 +	int res;
 +
 +	in_buf = pw_stream_dequeue_buffer(d->capture);
@@ -340,7 +913,6 @@ index 0000000..33054e4
 +				in_size > 7 ? bytes[7] : 0);
 +		d->logged_first_buffer = true;
 +	}
-+
 +	if (d->playback_failed)
 +		destroy_playback_stream(d);
 +
@@ -351,7 +923,33 @@ index 0000000..33054e4
 +		pw_stream_queue_buffer(d->capture, in_buf);
 +		return;
 +	}
-+
++	{
++		const uint8_t *pending = d->pending_data + d->pending_offset;
++		uint32_t available = d->pending_size - d->pending_offset;
++
++		if (available >= COMPRESS_RAW_AAC_HEADER_SIZE &&
++		    raw_aac_transport_header(pending, available, &raw_frame_size)) {
++			if (available < COMPRESS_RAW_AAC_HEADER_SIZE + raw_frame_size) {
++				pw_stream_queue_buffer(d->capture, in_buf);
++				return;
++			}
++			framed_raw_aac = true;
++			input_data = pending + COMPRESS_RAW_AAC_HEADER_SIZE;
++			in_size = raw_frame_size;
++		}
++	}
++	if (d->playback == NULL && !detect_payload_codec(d, input_data, in_size)) {
++		if (codec_is_auto(d->codec)) {
++			pw_log_debug("compress-forwarder source:%s waiting for codec metadata, preserved=%u",
++					d->monitor_name, d->pending_size - d->pending_offset);
++			pw_stream_queue_buffer(d->capture, in_buf);
++			return;
++		}
++		pw_log_error("compress-forwarder source:%s unsupported payload header for codec=%s",
++				d->monitor_name, d->codec);
++		pw_stream_queue_buffer(d->capture, in_buf);
++		return;
++	}
 +	if (d->playback == NULL) {
 +		request_playback_activation(d);
 +		if (d->playback == NULL) {
@@ -393,9 +991,22 @@ index 0000000..33054e4
 +	}
 +	copy_data = d->pending_data + d->pending_offset;
 +	copy_available = d->pending_size - d->pending_offset;
-+	copy_size = SPA_MIN(copy_available, out_size);
++	if (framed_raw_aac) {
++		if (raw_frame_size > out_size) {
++			pw_log_error("compress-forwarder raw AAC access unit too large size=%u output=%u",
++					raw_frame_size, out_size);
++			pw_stream_queue_buffer(d->playback, out_buf);
++			pw_stream_queue_buffer(d->capture, in_buf);
++			return;
++		}
++		copy_data = SPA_PTROFF(copy_data, COMPRESS_RAW_AAC_HEADER_SIZE, const void);
++		copy_size = raw_frame_size;
++	} else {
++		copy_size = SPA_MIN(copy_available, out_size);
++	}
 +	memcpy(out_data->data, copy_data, copy_size);
-+	d->pending_offset += copy_size;
++	d->pending_offset += copy_size +
++			(framed_raw_aac ? COMPRESS_RAW_AAC_HEADER_SIZE : 0);
 +	if (d->pending_offset >= d->pending_size)
 +		clear_pending_data(d);
 +
@@ -436,30 +1047,94 @@ index 0000000..33054e4
 +	.state_changed = playback_state_changed,
 +};
 +
++static void clear_client_node(struct forwarder_data *d)
++{
++	if (d->client_node == NULL)
++		return;
++	spa_hook_remove(&d->client_node_listener);
++	pw_proxy_destroy((struct pw_proxy *)d->client_node);
++	d->client_node = NULL;
++	d->client_node_id = PW_ID_ANY;
++}
++
++static void client_node_info(void *data, const struct pw_node_info *info)
++{
++	struct forwarder_data *d = data;
++
++	if (info->props == NULL || !update_codec_from_props(d, info->props))
++		return;
++	if (d->playback != NULL) {
++		pw_log_info("compress-forwarder final stream codec changed, recreating target codec=%s rate=%u channels=%u",
++				d->codec, d->rate, d->channels);
++		destroy_playback_stream(d);
++	}
++	if (d->capture_streaming && d->pending_size > d->pending_offset)
++		request_playback_activation(d);
++}
++
++static const struct pw_node_events client_node_events = {
++	PW_VERSION_NODE_EVENTS,
++	.info = client_node_info,
++};
++
 +static void registry_global(void *data, uint32_t id, uint32_t permissions,
 +		const char *type, uint32_t version, const struct spa_dict *props)
 +{
 +	struct forwarder_data *d = data;
-+	const char *name;
++	const char *name, *target, *media_class, *forwarder_source;
 +
 +	if (!spa_streq(type, PW_TYPE_INTERFACE_Node) || props == NULL)
 +		return;
 +	name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
-+	if (!spa_streq(name, d->target_name))
++	if (spa_streq(name, d->target_name)) {
++		d->target_id = id;
++		pw_log_debug("compress-forwarder target:%s resolved id=%u", d->target_name, id);
 +		return;
-+	d->target_id = id;
-+	pw_log_info("compress-forwarder target:%s resolved id=%u", d->target_name, id);
++	}
++
++	media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
++	target = spa_dict_lookup(props, PW_KEY_TARGET_OBJECT);
++	forwarder_source = spa_dict_lookup(props, "compress.forwarder.source");
++	if (spa_streq(media_class, "Stream/Output/Audio") &&
++	    !spa_streq(name, "compress-offload-forwarder-output") &&
++	    (spa_streq(forwarder_source, d->source_name) ||
++	     spa_streq(target, d->source_name) || spa_streq(target, d->monitor_name))) {
++		if (d->client_node != NULL && d->client_node_id != id) {
++			destroy_playback_stream(d);
++			clear_pending_data(d);
++		}
++		clear_client_node(d);
++		reset_codec_state(d);
++		update_codec_from_props(d, props);
++		d->client_node = pw_registry_bind(d->registry, id, type,
++				version < (uint32_t)PW_VERSION_NODE ? version : PW_VERSION_NODE, 0);
++		if (d->client_node == NULL) {
++			pw_log_error("compress-forwarder failed to bind client stream node id=%u", id);
++			return;
++		}
++		d->client_node_id = id;
++		pw_node_add_listener(d->client_node, &d->client_node_listener,
++				&client_node_events, d);
++		pw_log_debug("compress-forwarder tracking client stream node id=%u", id);
++	}
 +}
 +
 +static void registry_global_remove(void *data, uint32_t id)
 +{
 +	struct forwarder_data *d = data;
 +
-+	if (id != d->target_id)
-+		return;
-+	pw_log_info("compress-forwarder target:%s removed id=%u", d->target_name, id);
-+	d->target_id = PW_ID_ANY;
-+	destroy_playback_stream(d);
++	if (id == d->client_node_id) {
++		pw_log_debug("compress-forwarder client stream removed id=%u", id);
++		clear_client_node(d);
++		destroy_playback_stream(d);
++		clear_pending_data(d);
++		reset_codec_state(d);
++	}
++	if (id == d->target_id) {
++		pw_log_debug("compress-forwarder target:%s removed id=%u", d->target_name, id);
++		d->target_id = PW_ID_ANY;
++		destroy_playback_stream(d);
++	}
 +}
 +
 +static const struct pw_registry_events registry_events = {
@@ -477,6 +1152,13 @@ index 0000000..33054e4
 +	uint32_t n_params = 0;
 +	int res;
 +
++	if (codec_is_auto(d->codec))
++		return -EAGAIN;
++	if (codec_is_aac_raw(d->codec) && d->aac_audio_object_type == 0) {
++		pw_log_error("compress-forwarder raw AAC missing audio object type");
++		return -EINVAL;
++	}
++
 +	props = pw_properties_new(PW_KEY_MEDIA_CLASS, "Stream/Output/Audio",
 +			PW_KEY_NODE_NAME, "compress-offload-forwarder-output",
 +			PW_KEY_MEDIA_TYPE, "Audio",
@@ -486,7 +1168,6 @@ index 0000000..33054e4
 +			PW_KEY_NODE_AUTOCONNECT, "true",
 +			PW_KEY_STREAM_DONT_REMIX, "true",
 +			"node.dont-reconnect", "true",
-+			PW_KEY_NODE_RATE, "1/48000",
 +			"compress.offload", "true",
 +			"codec.type", d->codec,
 +			NULL);
@@ -495,6 +1176,16 @@ index 0000000..33054e4
 +	pw_properties_setf(props, "codec.sample_rate", "%u", d->rate);
 +	pw_properties_setf(props, "codec.channels", "%u", d->channels);
 +	pw_properties_setf(props, "codec.bit_rate", "%u", d->bitrate);
++	if (codec_is_aac(d->codec)) {
++		pw_properties_set(props, "codec.aac.stream_format",
++				d->aac_stream_format[0] != '\0' ? d->aac_stream_format :
++				(codec_is_aac_adts(d->codec) ? "adts" : "raw"));
++		pw_properties_setf(props, "codec.aac.audio_object_type", "%u",
++				d->aac_audio_object_type != 0 ? d->aac_audio_object_type :
++				(codec_is_aac_adts(d->codec) ? 2 : 0));
++		if (d->aac_codec_data[0] != '\0')
++			pw_properties_set(props, "codec.aac.codec_data", d->aac_codec_data);
++	}
 +	pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", d->rate);
 +
 +	d->playback = pw_stream_new(d->core, "compress offload forwarder output", props);
@@ -504,7 +1195,8 @@ index 0000000..33054e4
 +
 +	spa_pod_builder_init(&builder, buffer, sizeof(buffer));
 +	params[n_params++] = build_encoded_format(d, &builder, SPA_PARAM_EnumFormat);
-+	pw_log_debug("compress-forwarder target:%s connect target-id=%u", d->target_name, d->target_id);
++	pw_log_info("compress-forwarder target:%s connect target-id=%u codec=%s rate=%u channels=%u bitrate=%u",
++			d->target_name, d->target_id, d->codec, d->rate, d->channels, d->bitrate);
 +	res = pw_stream_connect(d->playback, PW_DIRECTION_OUTPUT, d->target_id,
 +			PW_STREAM_FLAG_AUTOCONNECT |
 +			PW_STREAM_FLAG_NO_CONVERT |
@@ -600,6 +1292,7 @@ index 0000000..33054e4
 +	res = create_capture_stream(d);
 +	if (res < 0)
 +		return res;
++	active_forwarder = d;
 +
 +	pw_log_info("compress-forwarder loaded: source=%s monitor=%s target=%s codec=%s rate=%u channels=%u",
 +			d->source_name, d->monitor_name, d->target_name,
@@ -612,12 +1305,16 @@ index 0000000..33054e4
 +{
 +	struct forwarder_data *d = module->user_data;
 +
++	if (active_forwarder == d)
++		active_forwarder = NULL;
++
 +	if (d->capture != NULL) {
 +		spa_hook_remove(&d->capture_listener);
 +		pw_stream_destroy(d->capture);
 +		d->capture = NULL;
 +	}
 +	destroy_playback_stream(d);
++	clear_client_node(d);
 +	if (d->registry != NULL) {
 +		spa_hook_remove(&d->registry_listener);
 +		pw_proxy_destroy((struct pw_proxy*)d->registry);
@@ -651,16 +1348,26 @@ index 0000000..33054e4
 +			str ? str : "pal_sink_speaker_compress");
 +	str = pw_properties_get(props, "codec");
 +	spa_scnprintf(d->codec, sizeof(d->codec), "%s", str ? str : DEFAULT_CODEC);
++	str = pw_properties_get(props, "codec.aac.stream_format");
++	spa_scnprintf(d->aac_stream_format, sizeof(d->aac_stream_format), "%s", str ? str : "");
++	str = pw_properties_get(props, "codec.aac.codec_data");
++	spa_scnprintf(d->aac_codec_data, sizeof(d->aac_codec_data), "%s", str ? str : "");
++	d->aac_audio_object_type = pw_properties_get_uint32(props, "codec.aac.audio_object_type", 0);
 +	d->rate = pw_properties_get_uint32(props, "rate", DEFAULT_RATE);
 +	d->channels = pw_properties_get_uint32(props, "channels", DEFAULT_CHANNELS);
 +	d->bitrate = pw_properties_get_uint32(props, "bitrate", DEFAULT_BITRATE);
 +	d->block_size = pw_properties_get_uint32(props, "block_size", DEFAULT_BLOCK_SIZE);
 +	spa_scnprintf(d->monitor_name, sizeof(d->monitor_name), "%s.monitor", d->source_name);
++	d->client_node_id = PW_ID_ANY;
 +
-+	if (!spa_streq(d->codec, "mp3")) {
-+		pw_log_warn("compress-forwarder only supports mp3 right now, requested codec=%s", d->codec);
++	if (!codec_is_auto(d->codec) && !codec_is_mp3(d->codec) && !codec_is_aac(d->codec)) {
++		pw_log_warn("compress-forwarder only supports auto, mp3, aac and aac_adts right now, requested codec=%s", d->codec);
 +		return -ENOTSUP;
 +	}
++	if (codec_is_aac_adts(d->codec) && d->aac_stream_format[0] == '\0')
++		spa_scnprintf(d->aac_stream_format, sizeof(d->aac_stream_format), "adts");
++	if (codec_is_aac_raw(d->codec) && d->aac_stream_format[0] == '\0')
++		spa_scnprintf(d->aac_stream_format, sizeof(d->aac_stream_format), "raw");
 +	if (d->channels == 0)
 +		d->channels = DEFAULT_CHANNELS;
 +	if (d->rate == 0)
@@ -672,9 +1379,8 @@ index 0000000..33054e4
 +}
 +
 +static const struct spa_dict_item module_compress_offload_forwarder_info[] = {
-+	{ PW_KEY_MODULE_AUTHOR,      "Qualcomm Technologies, Inc." },
-+	{ PW_KEY_MODULE_DESCRIPTION, "Forward Pulse compress-offload sink data to PAL compress sink" },
-+	{ PW_KEY_MODULE_USAGE,       forwarder_options },
++	{ PW_KEY_MODULE_DESCRIPTION, "Forward encoded PulseAudio streams to a compressed-offload sink" },
++	{ PW_KEY_MODULE_USAGE,       pulse_module_options },
 +	{ PW_KEY_MODULE_VERSION,     PACKAGE_VERSION },
 +};
 +
@@ -686,12 +1392,38 @@ index 0000000..33054e4
 +	.properties = &SPA_DICT_INIT_ARRAY(module_compress_offload_forwarder_info),
 +	.data_size  = sizeof(struct forwarder_data),
 +};
-diff --git a/src/modules/module-protocol-pulse/modules/module-compress-offload-sink.c b/src/modules/module-protocol-pulse/modules/module-compress-offload-sink.c
+diff --git a/src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-forwarder.h b/src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-forwarder.h
+new file mode 100644
+index 000000000..e936aad09
+--- /dev/null
++++ b/src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-forwarder.h
+@@ -0,0 +1,20 @@
++/* PipeWire */
++/* Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */
++/* SPDX-License-Identifier: BSD-3-Clause-Clear */
++
++#ifndef MODULE_COMPRESS_OFFLOAD_FORWARDER_H
++#define MODULE_COMPRESS_OFFLOAD_FORWARDER_H
++
++#include <spa/utils/dict.h>
++
++#define COMPRESS_RAW_AAC_MAGIC_0 'P'
++#define COMPRESS_RAW_AAC_MAGIC_1 'W'
++#define COMPRESS_RAW_AAC_MAGIC_2 'A'
++#define COMPRESS_RAW_AAC_MAGIC_3 'A'
++#define COMPRESS_RAW_AAC_HEADER_SIZE 8u
++
++void compress_offload_forwarder_update(const char *source_name,
++		const struct spa_dict *props);
++void compress_offload_forwarder_teardown(const char *source_name);
++
++#endif
+diff --git a/src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-sink.c b/src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-sink.c
 new file mode 100644
-index 0000000..243cc8e
+index 000000000..832131e68
 --- /dev/null
-+++ b/src/modules/module-protocol-pulse/modules/module-compress-offload-sink.c
-@@ -0,0 +1,293 @@
++++ b/src/modules/module-protocol-pulse/modules/pulse-module-compress-offload-sink.c
+@@ -0,0 +1,308 @@
 +/* PipeWire */
 +/* Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */
 +/* SPDX-License-Identifier: BSD-3-Clause-Clear */
@@ -704,11 +1436,22 @@ index 0000000..243cc8e
 +
 +#include "../module.h"
 +
++/** \page page_pulse_module_compress_offload_sink Compress Offload Sink
++ *
++ * ## Module Name
++ *
++ * `module-compress-offload-sink`
++ *
++ * ## Module Options
++ *
++ * @pulse_module_options@
++ */
++
 +static const char *const pulse_module_options =
 +	"sink_name=<name of sink> "
 +	"sink_properties=<properties for the sink> "
 +	"target_sink=<node.name of the downstream compress-offload sink> "
-+	"codec=<codec name, default mp3> "
++	"codec=<codec name, default auto> "
 +	"rate=<sample rate, default 44100> "
 +	"channels=<number of channels, default 2> "
 +	"bitrate=<bit rate, default 128000>";
@@ -927,7 +1670,8 @@ index 0000000..243cc8e
 +	}
 +
 +	str = pw_properties_get(props, "codec");
-+	pw_properties_set(props, "codec.type", str ? str : "mp3");
++	pw_properties_set(props, "codec.type", str ? str : "auto");
++	pw_properties_set(props, "codec.formats", "[ mp3 aac_adts aac ]");
 +	pw_properties_setf(props, "codec.sample_rate", "%u", rate);
 +	pw_properties_setf(props, "codec.channels",    "%u", channels);
 +	pw_properties_setf(props, "codec.bit_rate",    "%u", bitrate);
@@ -952,8 +1696,12 @@ index 0000000..243cc8e
 +	pw_properties_set(props, PW_KEY_NODE_VIRTUAL,                "true");
 +	pw_properties_set(props, PW_KEY_PRIORITY_SESSION,            "1");
 +	pw_properties_set(props, PW_KEY_PRIORITY_DRIVER,             "1");
-+	pw_properties_set(props, "monitor.channel-volumes",          "true");
++	pw_properties_set(props, "monitor.channel-volumes",          "false");
 +	pw_properties_set(props, "monitor.passthrough",              "true");
++	pw_properties_set(props, "channelmix.disable",                "true");
++	pw_properties_set(props, "resample.disable",                  "true");
++	pw_properties_set(props, "compress.carrier.rate",             "48000");
++	pw_properties_set(props, "compress.carrier.channels",         "2");
 +	pw_properties_set(props, "compress.offload",                 "true");
 +	pw_properties_set(props, PW_KEY_FACTORY_NAME,                "support.null-audio-sink");
 +
@@ -971,7 +1719,6 @@ index 0000000..243cc8e
 +}
 +
 +static const struct spa_dict_item module_compress_offload_sink_info[] = {
-+	{ PW_KEY_MODULE_AUTHOR,      "Qualcomm Technologies, Inc." },
 +	{ PW_KEY_MODULE_DESCRIPTION, "Pulse-visible alias sink for compressed offload playback" },
 +	{ PW_KEY_MODULE_USAGE,       pulse_module_options },
 +	{ PW_KEY_MODULE_VERSION,     PACKAGE_VERSION },
@@ -985,24 +1732,70 @@ index 0000000..243cc8e
 +	.properties = &SPA_DICT_INIT_ARRAY(module_compress_offload_sink_info),
 +	.data_size  = sizeof(struct module_compress_offload_sink_data),
 +};
+-- 
+GitLab
+
+
+From 574fec7ebc482a462b2842422cec156aa9a43f1e Mon Sep 17 00:00:00 2001
+From: Harsh Rai <harsh.rai@oss.qualcomm.com>
+Date: Fri, 23 Oct 2026 17:45:41 +0530
+Subject: [PATCH 4/4] pulse-server: preserve encoded streams for compressed
+ offload
+
+Recognize compressed-offload sinks when Pulse playback streams are
+created and advertise MP3, AAC ADTS, and raw AAC formats with their
+codec parameters.
+
+Configure compressed streams for byte-preserving transport: use a
+single-byte frame size, disable format conversion, remove playback
+prebuffering, and connect the stream with an encoded format while
+retaining the negotiated codec properties.
+
+Frame each raw AAC access unit with its payload length before writing it
+to the Pulse ring buffer. Include the framing bytes in buffer-capacity
+checks and use the stream's allocated buffer size when wrapping the
+write position.
+
+Track the forwarding source on each stream and notify the forwarder when
+the Pulse playback stream is freed. This lets the target stream and any
+pending encoded data be released when playback ends or the client is
+removed.
+
+Upstream-Status: Submitted [https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/2878]
+Signed-off-by: Harsh Rai <harsh.rai@oss.qualcomm.com>
+---
+ .../module-protocol-pulse/pulse-server.c      | 654 +++++++++++++++++-
+ src/modules/module-protocol-pulse/server.c    |  35 +-
+ src/modules/module-protocol-pulse/stream.c    |   5 +
+ src/modules/module-protocol-pulse/stream.h    |   2 +
+ 4 files changed, 675 insertions(+), 21 deletions(-)
+
 diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c
-index eb3233b..61f0dfb 100644
+index 153be0c4f..d8a8591d8 100644
 --- a/src/modules/module-protocol-pulse/pulse-server.c
 +++ b/src/modules/module-protocol-pulse/pulse-server.c
-@@ -84,6 +84,9 @@ struct temporary_move_data {
+@@ -34,6 +34,7 @@
+ #include <pipewire/extensions/metadata.h>
+ 
+ #include "pulse-server.h"
++#include "modules/pulse-module-compress-offload-forwarder.h"
+ #include "client.h"
+ #include "collect.h"
+ #include "commands.h"
+@@ -89,6 +90,9 @@ struct temporary_move_data {
  	uint8_t used:1;
  };
-
+ 
 +/* Buffer size used for compress-offload streams (frame_size == 1). */
 +#define COMPRESS_OFFLOAD_BUF_SIZE  16484u
 +
  static struct sample *find_sample(struct impl *impl, uint32_t index, const char *name)
  {
  	union pw_map_item *item;
-@@ -1199,10 +1202,18 @@ static const struct spa_pod *get_buffers_param(struct stream *s,
+@@ -1223,10 +1227,18 @@ static const struct spa_pod *get_buffers_param(struct stream *s,
  	blocks = 1;
  	stride = s->frame_size;
-
+ 
 +	/* compressed offload uses byte granularity */
 +	if (s->frame_size == 1) {
 +		size = COMPRESS_OFFLOAD_BUF_SIZE;
@@ -1011,17 +1804,17 @@ index eb3233b..61f0dfb 100644
 +	}
 +
  	size = defs->quantum_limit * s->frame_size;
-
+ 
  	pw_log_info("[%s] stride %d size %u", s->client->name, stride, size);
-
+ 
 +build_buffers_param:
  	param = spa_pod_builder_add_object(b,
  			SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
  			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(MIN_BUFFERS,
-@@ -1228,6 +1239,23 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *
+@@ -1252,6 +1264,23 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *
  	if (id != SPA_PARAM_Format || param == NULL)
  		return;
-
+ 
 +	/* For compress-offload streams, force byte-granularity regardless
 +	 * of negotiated format (we connect with S16LE but data is compressed). */
 +	{
@@ -1042,30 +1835,32 @@ index eb3233b..61f0dfb 100644
  	if ((res = format_parse_param(param, false, &stream->ss, &stream->map, NULL, NULL)) < 0) {
  		pw_stream_set_error(stream->stream, res, "format not supported");
  		return;
-@@ -1243,6 +1271,9 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *
+@@ -1267,6 +1296,9 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *
  		return;
  	}
  	stream->rate = stream->ss.rate;
 +stream_param_format_done:
 +	pw_log_debug("[%s] stream param done: frame_size=%u rate=%u ss.format=%u",
 +			stream->client->name, stream->frame_size, stream->rate, stream->ss.format);
-
+ 
  	if (stream->create_tag != SPA_ID_INVALID) {
  		struct pw_manager_object *peer;
-@@ -1583,6 +1614,98 @@ static void log_format_info(struct impl *impl, enum spa_log_level level, struct
+@@ -1621,6 +1653,395 @@ static void log_format_info(struct impl *impl, enum spa_log_level level, struct
  				impl, it->key, it->value);
  }
-
+ 
 +static const struct spa_pod *build_compress_offload_format(struct spa_pod_builder *b,
 +		uint32_t id, const struct pw_properties *props, uint32_t *rate)
 +{
 +	const char *codec;
++	const char *stream_format;
 +	struct spa_audio_info info;
 +	uint32_t sample_rate, channels, bitrate;
 +
 +	spa_zero(info);
 +	info.media_type = SPA_MEDIA_TYPE_audio;
 +	codec = pw_properties_get(props, "codec.type");
++	stream_format = pw_properties_get(props, "codec.aac.stream_format");
 +	sample_rate = pw_properties_get_uint32(props, "codec.sample_rate", 44100);
 +	channels = pw_properties_get_uint32(props, "codec.channels", 2);
 +	bitrate = pw_properties_get_uint32(props, "codec.bit_rate", 128000);
@@ -1081,7 +1876,12 @@ index eb3233b..61f0dfb 100644
 +		info.info.aac.rate = sample_rate;
 +		info.info.aac.channels = channels;
 +		info.info.aac.bitrate = bitrate;
-+		info.info.aac.stream_format = SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS;
++		info.info.aac.audio_object_type = pw_properties_get_uint32(props,
++				"codec.aac.audio_object_type", 0);
++		info.info.aac.stream_format = spa_streq(codec, "aac_adts") ||
++				stream_format == NULL || spa_streq(stream_format, "adts") ?
++				SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS :
++				SPA_AUDIO_AAC_STREAM_FORMAT_RAW;
 +	} else if (spa_streq(codec, "aac_adif")) {
 +		info.media_subtype = SPA_MEDIA_SUBTYPE_aac;
 +		info.info.aac.rate = sample_rate;
@@ -1131,9 +1931,10 @@ index eb3233b..61f0dfb 100644
 +		struct channel_map *map, const struct pw_properties *props)
 +{
 +	uint32_t channels;
++
 +	ss->format = SPA_AUDIO_FORMAT_S16_LE;
-+	ss->rate = pw_properties_get_uint32(props, "codec.sample_rate", 44100);
-+	channels = pw_properties_get_uint32(props, "codec.channels", 2);
++	ss->rate = pw_properties_get_uint32(props, "compress.carrier.rate", 48000);
++	channels = pw_properties_get_uint32(props, "compress.carrier.channels", 2);
 +	ss->channels = (uint8_t)(channels > 0 ? channels : 2);
 +	spa_zero(*map);
 +	switch (ss->channels) {
@@ -1147,24 +1948,314 @@ index eb3233b..61f0dfb 100644
 +	default: channel_map_parse("stereo", map); ss->channels = 2; break;
 +	}
 +}
++
++static uint32_t compress_format_get_u32_prop(const struct format_info *format,
++		const char *key, uint32_t default_value)
++{
++	const char *str, *val;
++	struct spa_json it[2];
++	int len, value;
++
++	if (format->props == NULL || (str = pw_properties_get(format->props, key)) == NULL)
++		return default_value;
++	if ((len = spa_json_begin(&it[0], str, strlen(str), &val)) <= 0)
++		return default_value;
++	if (spa_json_is_int(val, len) && spa_json_parse_int(val, len, &value) > 0 && value > 0)
++		return (uint32_t)value;
++	if ((value = atoi(str)) > 0)
++		return (uint32_t)value;
++	return default_value;
++}
++
++static const char *compress_format_get_string_prop(const struct format_info *format,
++		const char *key, char *buffer, size_t size)
++{
++	const char *str, *val;
++	struct spa_json it[2];
++	int len;
++
++	if (format->props == NULL || (str = pw_properties_get(format->props, key)) == NULL)
++		return NULL;
++	if ((len = spa_json_begin(&it[0], str, strlen(str), &val)) > 0 && spa_json_is_string(val, len))
++		return spa_scnprintf(buffer, size, "%.*s", len - 2, val + 1) >= 0 ? buffer : NULL;
++	return str;
++}
++
++static uint32_t compress_format_get_u32_prop_compat(const struct format_info *format,
++		const char *primary, const char *compat, uint32_t default_value)
++{
++	uint32_t value;
++
++	value = compress_format_get_u32_prop(format, primary, 0);
++	if (value == 0 && compat != NULL)
++		value = compress_format_get_u32_prop(format, compat, 0);
++	return value != 0 ? value : default_value;
++}
++
++static const char *compress_format_get_string_prop_compat(
++		const struct format_info *format, const char *primary,
++		const char *compat, char *buffer, size_t size)
++{
++	const char *str;
++
++	str = compress_format_get_string_prop(format, primary, buffer, size);
++	if (str == NULL && compat != NULL)
++		str = compress_format_get_string_prop(format, compat, buffer, size);
++	return str;
++}
++
++static bool compress_format_stream_format_is(const struct format_info *format, const char *expected)
++{
++	char stream_format[32];
++	const char *str;
++
++	str = compress_format_get_string_prop_compat(format, "format.stream_format",
++			"stream-format", stream_format, sizeof(stream_format));
++	return str == NULL ? spa_streq(expected, "adts") : spa_streq(str, expected);
++}
++
++static bool compress_hex_nibble(char c, uint8_t *value)
++{
++	if (c >= '0' && c <= '9')
++		*value = c - '0';
++	else if (c >= 'a' && c <= 'f')
++		*value = c - 'a' + 10;
++	else if (c >= 'A' && c <= 'F')
++		*value = c - 'A' + 10;
++	else
++		return false;
++	return true;
++}
++
++static uint32_t compress_read_bits(const uint8_t *data, size_t size,
++		uint32_t *bit_offset, uint32_t n_bits)
++{
++	uint32_t value = 0, i;
++
++	if (*bit_offset + n_bits > size * 8)
++		return SPA_ID_INVALID;
++	for (i = 0; i < n_bits; i++) {
++		uint32_t byte = (*bit_offset + i) / 8;
++		uint32_t bit = 7 - ((*bit_offset + i) % 8);
++		value = (value << 1) | ((data[byte] >> bit) & 1);
++	}
++	*bit_offset += n_bits;
++	return value;
++}
++
++struct compress_aac_config {
++	uint32_t object_type;
++	uint32_t rate;
++	uint32_t channels;
++};
++
++static uint32_t compress_aac_sample_rate(const uint8_t *data, size_t size,
++		uint32_t *bit_offset)
++{
++	static const uint32_t rates[] = {
++		96000, 88200, 64000, 48000, 44100, 32000, 24000,
++		22050, 16000, 12000, 11025, 8000, 7350,
++	};
++	uint32_t index = compress_read_bits(data, size, bit_offset, 4);
++
++	if (index == SPA_ID_INVALID)
++		return 0;
++	if (index == 15) {
++		uint32_t rate = compress_read_bits(data, size, bit_offset, 24);
++		return rate == SPA_ID_INVALID ? 0 : rate;
++	}
++	return index < SPA_N_ELEMENTS(rates) ? rates[index] : 0;
++}
++
++static uint32_t compress_aac_object_type(const uint8_t *data, size_t size,
++		uint32_t *bit_offset)
++{
++	uint32_t object_type = compress_read_bits(data, size, bit_offset, 5);
++
++	if (object_type == SPA_ID_INVALID)
++		return 0;
++	if (object_type == 31) {
++		uint32_t ext = compress_read_bits(data, size, bit_offset, 6);
++		if (ext == SPA_ID_INVALID)
++			return 0;
++		object_type = 32 + ext;
++	}
++	return object_type;
++}
++
++static bool compress_parse_audio_specific_config(const char *str,
++		struct compress_aac_config *config)
++{
++	uint8_t data[64];
++	uint32_t bit_offset = 0, channel_config, extension_rate;
++	size_t n_data = 0;
++	bool high = true;
++	uint8_t byte = 0;
++	static const uint8_t channel_count[] = { 0, 1, 2, 3, 4, 5, 6, 8 };
++
++	if (str == NULL || config == NULL)
++		return false;
++	spa_zero(*config);
++	if (spa_strstartswith(str, "(buffer)"))
++		str += strlen("(buffer)");
++	if (spa_strstartswith(str, "0x") || spa_strstartswith(str, "0X"))
++		str += 2;
++	for (; *str != '\0' && n_data < sizeof(data); str++) {
++		uint8_t nibble;
++
++		if (!compress_hex_nibble(*str, &nibble))
++			continue;
++		if (high) {
++			byte = nibble << 4;
++			high = false;
++		} else {
++			data[n_data++] = byte | nibble;
++			high = true;
++		}
++	}
++	if (!high || n_data == 0)
++		return false;
++
++	config->object_type = compress_aac_object_type(data, n_data, &bit_offset);
++	config->rate = compress_aac_sample_rate(data, n_data, &bit_offset);
++	channel_config = compress_read_bits(data, n_data, &bit_offset, 4);
++	if (config->object_type == 0 || config->rate == 0 ||
++	    channel_config == SPA_ID_INVALID)
++		return false;
++	if (channel_config < SPA_N_ELEMENTS(channel_count))
++		config->channels = channel_count[channel_config];
++
++	if (config->object_type == 5 || config->object_type == 29) {
++		extension_rate = compress_aac_sample_rate(data, n_data, &bit_offset);
++		if (extension_rate != 0)
++			config->rate = extension_rate;
++	}
++	return true;
++}
++
++static int set_compress_offload_props_from_format(struct pw_properties *props,
++		const struct format_info *format)
++{
++	const char *codec, *stream_format;
++	char stream_format_buffer[32];
++	bool is_aac = false;
++
++	switch (format->encoding) {
++	case ENCODING_MP3:
++		codec = "mp3";
++		break;
++	case ENCODING_AAC:
++		if (!compress_format_stream_format_is(format, "raw"))
++			return -ENOTSUP;
++		codec = "aac";
++		is_aac = true;
++		break;
++	case ENCODING_AAC_ADTS:
++		if (!compress_format_stream_format_is(format, "adts"))
++			return -ENOTSUP;
++		codec = "aac_adts";
++		is_aac = true;
++		break;
++	case ENCODING_MPEG2_AAC_IEC61937:
++		stream_format = compress_format_get_string_prop_compat(format,
++				"format.stream_format", "stream-format",
++				stream_format_buffer, sizeof(stream_format_buffer));
++		if (stream_format == NULL || spa_streq(stream_format, "adts"))
++			codec = "aac_adts";
++		else if (spa_streq(stream_format, "raw"))
++			codec = "aac";
++		else
++			return -ENOTSUP;
++		is_aac = true;
++		break;
++	default:
++		return -ENOTSUP;
++	}
++
++	pw_properties_set(props, "codec.type", codec);
++	pw_properties_setf(props, "codec.sample_rate", "%u",
++			compress_format_get_u32_prop(format, "format.rate",
++				pw_properties_get_uint32(props, "codec.sample_rate", 48000)));
++	pw_properties_setf(props, "codec.channels", "%u",
++			compress_format_get_u32_prop_compat(format, "format.channels", "channels",
++				pw_properties_get_uint32(props, "codec.channels", 2)));
++	pw_properties_setf(props, "codec.bit_rate", "%u",
++			compress_format_get_u32_prop_compat(format, "format.bit_rate", "bitrate",
++				pw_properties_get_uint32(props, "codec.bit_rate", 128000)));
++	if (is_aac) {
++		char stream_format[32], codec_data[256];
++		const char *str;
++		struct compress_aac_config aac_config;
++		uint32_t object_type = compress_format_get_u32_prop_compat(format,
++				"format.audio_object_type", "audio-object-type",
++				pw_properties_get_uint32(props, "codec.aac.audio_object_type", 0));
++
++		str = compress_format_get_string_prop_compat(format, "format.stream_format",
++				"stream-format", stream_format, sizeof(stream_format));
++		pw_properties_set(props, "codec.aac.stream_format",
++				str != NULL ? str : (spa_streq(codec, "aac_adts") ? "adts" : "raw"));
++		str = compress_format_get_string_prop_compat(format, "format.codec_data",
++				"codec_data", codec_data, sizeof(codec_data));
++		if (str == NULL)
++			str = compress_format_get_string_prop(format, "codec-data",
++					codec_data, sizeof(codec_data));
++		if (str == NULL)
++			str = pw_properties_get(props, "codec.aac.codec_data");
++		if (str != NULL) {
++			pw_properties_set(props, "codec.aac.codec_data", str);
++			if (compress_parse_audio_specific_config(str, &aac_config)) {
++				if (object_type == 0)
++					object_type = aac_config.object_type;
++				if (aac_config.rate != 0)
++					pw_properties_setf(props, "codec.sample_rate", "%u", aac_config.rate);
++				if (aac_config.channels != 0)
++					pw_properties_setf(props, "codec.channels", "%u", aac_config.channels);
++			}
++		}
++		if (object_type == 0 && spa_streq(codec, "aac_adts"))
++			object_type = 2;
++		if (object_type != 0)
++			pw_properties_setf(props, "codec.aac.audio_object_type", "%u", object_type);
++	}
++	return 0;
++}
++
++static int init_compress_format_info(struct format_info *format,
++		enum encoding encoding, uint32_t rate, uint32_t channels, uint32_t bitrate)
++{
++	spa_zero(*format);
++	format->encoding = encoding;
++	format->props = pw_properties_new(NULL, NULL);
++	if (format->props == NULL)
++		return -errno;
++	pw_properties_setf(format->props, "format.rate", "%u", rate);
++	pw_properties_setf(format->props, "format.channels", "%u", channels);
++	if (encoding == ENCODING_AAC || encoding == ENCODING_AAC_ADTS) {
++		pw_properties_setf(format->props, "format.bit_rate", "%u", bitrate);
++		pw_properties_set(format->props, "format.stream_format",
++				encoding == ENCODING_AAC_ADTS ? "\"adts\"" : "\"raw\"");
++	}
++	return 0;
++}
 +
  static int do_create_playback_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
  {
  	struct impl *impl = client->impl;
-@@ -1609,7 +1732,8 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
+@@ -1646,7 +2067,9 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
  		muted_set = false,
  		fail_on_suspend = false,
  		relative_volume = false,
 -		passthrough = false;
 +		passthrough = false,
-+		compress_offload = false;
++		compress_offload = false,
++		compress_format_selected = false;
  	struct volume volume;
  	struct pw_properties *props = NULL;
  	uint8_t n_formats = 0;
-@@ -1668,6 +1792,68 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
+@@ -1705,6 +2128,70 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
  	}
  	o = find_device(client, sink_index, sink_name, true, &is_monitor);
-
+ 
 +	if (o != NULL) {
 +		const struct pw_node_info *ninfo = o->info;
 +		const struct spa_dict *sp = (ninfo && ninfo->props) ? ninfo->props : NULL;
@@ -1209,8 +2300,10 @@ index eb3233b..61f0dfb 100644
 +			channels = channels_buf[0] ? channels_buf : NULL;
 +			bitrate = bitrate_buf[0] ? bitrate_buf : NULL;
 +
-+			pw_properties_set(props, "compress.offload", "true");
-+			pw_properties_set(props, "item.node.supports-encoded-fmts", "true");
++				pw_properties_set(props, "compress.offload", "true");
++				if (node_name != NULL)
++					pw_properties_set(props, "compress.forwarder.source", node_name);
++				pw_properties_set(props, "item.node.supports-encoded-fmts", "true");
 +			pw_properties_set(props, PW_KEY_MEDIA_TYPE, "Audio");
 +			pw_properties_set(props, PW_KEY_MEDIA_CATEGORY, "Playback");
 +			pw_properties_set(props, PW_KEY_MEDIA_ROLE, "Music");
@@ -1230,28 +2323,49 @@ index eb3233b..61f0dfb 100644
  	spa_zero(fix_ss);
  	spa_zero(fix_map);
  	if ((fix_format || fix_rate || fix_channels) && o != NULL) {
-@@ -1715,6 +1901,9 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
+@@ -1752,6 +2239,27 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
  			goto error_protocol;
  	}
-
+ 
 +	compress_offload = pw_properties_parse_bool(
 +			pw_properties_get(props, "compress.offload"));
++	if (!compress_offload && sink_name != NULL &&
++	    spa_streq(sink_name, "pal_speaker_compress")) {
++		compress_offload = true;
++		pw_properties_set(props, "compress.offload", "true");
++		pw_properties_set(props, "compress.forwarder.source", "pal_speaker_compress");
++		pw_properties_set(props, "compress.target.object", "pal_sink_speaker_compress");
++		pw_properties_set(props, "item.node.supports-encoded-fmts", "true");
++		if (pw_properties_get(props, "codec.type") == NULL)
++			pw_properties_set(props, "codec.type", "mp3");
++		if (pw_properties_get(props, "codec.sample_rate") == NULL)
++			pw_properties_set(props, "codec.sample_rate", "48000");
++		if (pw_properties_get(props, "codec.channels") == NULL)
++			pw_properties_set(props, "codec.channels", "2");
++		if (pw_properties_get(props, "codec.bit_rate") == NULL)
++			pw_properties_set(props, "codec.bit_rate", "128000");
++		pw_log_debug("[%s] using byte-preserving compress-offload path for sink=%s",
++				client->name, sink_name);
++	}
 +
  	if (client->version >= 21) {
  		if (message_get(m,
  				TAG_U8, &n_formats,
-@@ -1732,7 +1921,9 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
+@@ -1769,7 +2277,12 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
  						TAG_INVALID) < 0)
  					goto error_protocol;
-
+ 
 -				if (n_params < MAX_FORMATS &&
 +				if (compress_offload) {
 +					log_format_info(impl, SPA_LOG_LEVEL_DEBUG, &format);
++					if (!compress_format_selected &&
++					    set_compress_offload_props_from_format(props, &format) == 0)
++						compress_format_selected = true;
 +				} else if (n_params < MAX_FORMATS &&
  				    (params[n_params] = format_info_build_param(&b,
  						SPA_PARAM_EnumFormat, &format, &r)) != NULL) {
  					n_params++;
-@@ -1746,7 +1937,23 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
+@@ -1783,7 +2296,26 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
  			}
  		}
  	}
@@ -1272,32 +2386,64 @@ index eb3233b..61f0dfb 100644
 +			pw_log_warn("%p: unsupported compress codec:%s",
 +					impl, pw_properties_get(props, "codec.type"));
 +		}
++		compress_offload_forwarder_update(
++				pw_properties_get(props, "compress.forwarder.source"),
++				&props->dict);
 +	} else if (sample_spec_valid(&ss)) {
  		struct sample_spec sfix = ss;
  		struct channel_map mfix = map;
-
-@@ -1770,6 +1977,8 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
+ 
+@@ -1807,6 +2339,8 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
  	if (m->offset != m->length)
  		goto error_protocol;
-
+ 
 +	pw_log_debug("[%s] format check: n_valid=%u n_params=%u compress=%d passthrough=%d",
 +			client->name, n_valid_formats, n_params, compress_offload, passthrough);
  	if (n_valid_formats == 0)
  		goto error_no_formats;
-
-@@ -1806,6 +2015,8 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
+ 
+@@ -1816,6 +2350,16 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
+ 	stream = stream_new(client, STREAM_TYPE_PLAYBACK, tag, &ss, &map, &attr);
+ 	if (stream == NULL)
+ 		goto error_errno;
++	stream->compress_offload = compress_offload;
++	if (compress_offload) {
++		const char *forwarder_source =
++				pw_properties_get(props, "compress.forwarder.source");
++
++		if (forwarder_source != NULL)
++			spa_scnprintf(stream->compress_forwarder_source,
++					sizeof(stream->compress_forwarder_source), "%s",
++					forwarder_source);
++	}
+ 
+ 	stream->corked = corked;
+ 	stream->is_paused = corked;
+@@ -1835,6 +2379,11 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
+ 	if (rate != 0) {
+ 		struct spa_fraction lat;
+ 		fix_playback_buffer_attr(stream, &attr, ss_rate, &lat);
++		if (compress_offload) {
++			attr.prebuf = 0;
++			pw_log_debug("[%s] compress-offload uses immediate playback prebuf=0",
++					client->name);
++		}
+ 		pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", rate);
+ 		pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u",
+ 				lat.num, lat.denom);
+@@ -1844,6 +2393,8 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
  	flags = 0;
  	if (no_move)
  		flags |= PW_STREAM_FLAG_DONT_RECONNECT;
 +	if (compress_offload)
 +		flags |= PW_STREAM_FLAG_NO_CONVERT;
-
- 	if (sink_name != NULL) {
- 		if (o != NULL)
-@@ -1833,14 +2044,35 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
+ 	if (corked)
+ 		flags |= PW_STREAM_FLAG_INACTIVE;
+ 
+@@ -1875,14 +2426,35 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
  			&stream->stream_listener,
  			&stream_events, stream);
-
+ 
 -	pw_stream_connect(stream->stream,
 -			PW_DIRECTION_OUTPUT,
 -			SPA_ID_INVALID,
@@ -1335,9 +2481,215 @@ index eb3233b..61f0dfb 100644
 +				PW_STREAM_FLAG_MAP_BUFFERS,
 +				params, n_params);
 +	}
-
+ 
  	stream_update_tag_param(stream);
-
---
-2.43.0
-
+ 
+@@ -3927,9 +4499,50 @@ static int fill_sink_info(struct client *client, struct message *m,
+ 	}
+ 	if (client->version >= 21) {
+ 		struct pw_manager_param *p;
+-		struct format_info info[32];
++		struct format_info formats[32];
+ 		uint32_t i, n_info = 0;
+ 		struct spa_list *param_list = pw_manager_object_get_param_list(o);
++		bool compress_offload = pw_properties_parse_bool(
++				spa_dict_lookup(info->props, "compress.offload"));
++		uint32_t compress_rate = 48000, compress_channels = 2, compress_bitrate = 128000;
++
++		if (compress_offload) {
++			const char *str;
++
++			if ((str = spa_dict_lookup(info->props, "codec.sample_rate")) != NULL)
++				compress_rate = (uint32_t)atoi(str);
++			if ((str = spa_dict_lookup(info->props, "codec.channels")) != NULL)
++				compress_channels = (uint32_t)atoi(str);
++			if ((str = spa_dict_lookup(info->props, "codec.bit_rate")) != NULL)
++				compress_bitrate = (uint32_t)atoi(str);
++			if (compress_rate == 0)
++				compress_rate = 48000;
++			if (compress_channels == 0)
++				compress_channels = 2;
++			if (compress_bitrate == 0)
++				compress_bitrate = 128000;
++			if (n_info < SPA_N_ELEMENTS(formats) &&
++			    init_compress_format_info(&formats[n_info], ENCODING_MP3,
++					compress_rate, compress_channels, compress_bitrate) == 0)
++				n_info++;
++			if (n_info < SPA_N_ELEMENTS(formats) &&
++			    init_compress_format_info(&formats[n_info], ENCODING_AAC_ADTS,
++					compress_rate, compress_channels, compress_bitrate) == 0)
++				n_info++;
++			if (n_info < SPA_N_ELEMENTS(formats) &&
++			    init_compress_format_info(&formats[n_info], ENCODING_AAC,
++					48000, 2, compress_bitrate) == 0)
++				n_info++;
++			if (n_info < SPA_N_ELEMENTS(formats) &&
++			    init_compress_format_info(&formats[n_info], ENCODING_AAC,
++					16000, 1, compress_bitrate) == 0)
++				n_info++;
++			if (n_info < SPA_N_ELEMENTS(formats) &&
++			    init_compress_format_info(&formats[n_info], ENCODING_AAC,
++					12000, 1, compress_bitrate) == 0)
++				n_info++;
++			goto send_formats;
++		}
+ 
+ 		spa_list_for_each(p, param_list, link) {
+ 			uint32_t index = 0;
+@@ -3937,26 +4550,27 @@ static int fill_sink_info(struct client *client, struct message *m,
+ 			if (p->id != SPA_PARAM_EnumFormat)
+ 				continue;
+ 
+-			while (n_info < SPA_N_ELEMENTS(info)) {
+-				spa_zero(info[n_info]);
+-				if (format_info_from_param(&info[n_info], p->param, index++) < 0)
++			while (n_info < SPA_N_ELEMENTS(formats)) {
++				spa_zero(formats[n_info]);
++				if (format_info_from_param(&formats[n_info], p->param, index++) < 0)
+ 					break;
+-				if (info[n_info].encoding == ENCODING_ANY ||
+-				    (info[n_info].encoding == ENCODING_PCM && info[n_info].props != NULL)) {
+-					format_info_clear(&info[n_info]);
++				if (formats[n_info].encoding == ENCODING_ANY ||
++				    (formats[n_info].encoding == ENCODING_PCM && formats[n_info].props != NULL)) {
++					format_info_clear(&formats[n_info]);
+ 					continue;
+ 				}
+ 				n_info++;
+ 			}
+ 		}
++	send_formats:
+ 		message_put(m,
+ 			TAG_U8, n_info,				/* n_formats */
+ 			TAG_INVALID);
+ 		for (i = 0; i < n_info; i++) {
+ 			message_put(m,
+-				TAG_FORMAT_INFO, &info[i],
++				TAG_FORMAT_INFO, &formats[i],
+ 				TAG_INVALID);
+-			format_info_clear(&info[i]);
++			format_info_clear(&formats[i]);
+ 		}
+ 	}
+ 	return 0;
+diff --git a/src/modules/module-protocol-pulse/server.c b/src/modules/module-protocol-pulse/server.c
+index c00ea8e55..ae8091403 100644
+--- a/src/modules/module-protocol-pulse/server.c
++++ b/src/modules/module-protocol-pulse/server.c
+@@ -33,6 +33,7 @@
+ #include "commands.h"
+ #include "defs.h"
+ #include "internal.h"
++#include "modules/pulse-module-compress-offload-forwarder.h"
+ #include "log.h"
+ #include "message.h"
+ #include "reply.h"
+@@ -113,9 +114,13 @@ static int handle_memblock(struct client *client, struct message *msg)
+ {
+ 	struct stream *stream;
+ 	uint32_t channel, flags, index;
++	uint32_t transport_size;
+ 	int64_t offset, diff;
+ 	int32_t filled;
+ 	int res = 0;
++	bool frame_raw_aac;
++	const char *compress_offload;
++	uint8_t raw_aac_header[COMPRESS_RAW_AAC_HEADER_SIZE];
+ 
+ 	channel = ntohl(client->desc.channel);
+ 	offset = (int64_t) (
+@@ -133,6 +138,25 @@ static int handle_memblock(struct client *client, struct message *msg)
+ 			    client, client->name, channel);
+ 		goto finish;
+ 	}
++	compress_offload = stream->props != NULL ?
++			pw_properties_get(stream->props, "compress.offload") : NULL;
++	frame_raw_aac = compress_offload != NULL && spa_atob(compress_offload);
++	transport_size = msg->length;
++	if (frame_raw_aac) {
++		if (msg->length > UINT32_MAX - COMPRESS_RAW_AAC_HEADER_SIZE) {
++			res = -EOVERFLOW;
++			goto finish;
++		}
++		transport_size += COMPRESS_RAW_AAC_HEADER_SIZE;
++		raw_aac_header[0] = COMPRESS_RAW_AAC_MAGIC_0;
++		raw_aac_header[1] = COMPRESS_RAW_AAC_MAGIC_1;
++		raw_aac_header[2] = COMPRESS_RAW_AAC_MAGIC_2;
++		raw_aac_header[3] = COMPRESS_RAW_AAC_MAGIC_3;
++		raw_aac_header[4] = msg->length & 0xff;
++		raw_aac_header[5] = (msg->length >> 8) & 0xff;
++		raw_aac_header[6] = (msg->length >> 16) & 0xff;
++		raw_aac_header[7] = (msg->length >> 24) & 0xff;
++	}
+ 
+ 	filled = spa_ringbuffer_get_write_index(&stream->ring, &index);
+ 	pw_log_trace("new block %p %p/%u filled:%d index:%d flags:%02x offset:%" PRIu64,
+@@ -173,13 +197,22 @@ static int handle_memblock(struct client *client, struct message *msg)
+ 
+ 	if (filled < 0) {
+ 		/* underrun, reported on reader side */
+-	} else if (filled + msg->length > stream->attr.maxlength) {
++	} else if (filled + transport_size > stream->attr.maxlength) {
+ 		/* overrun */
+ 		stream_send_overflow(stream);
+ 	}
+ 
+ 	/* always write data to ringbuffer, we expect the other side
+ 	 * to recover */
++	if (frame_raw_aac) {
++		spa_ringbuffer_write_data(&stream->ring,
++				stream->buffer, stream->bufsize,
++				index % stream->bufsize,
++				raw_aac_header, sizeof(raw_aac_header));
++		index += sizeof(raw_aac_header);
++		pw_log_debug("client %p [%s]: framed raw AAC access unit size=%u",
++				client, client->name, msg->length);
++	}
+ 	spa_ringbuffer_write_data(&stream->ring,
+ 			stream->buffer, stream->bufsize,
+ 			index % stream->bufsize,
+diff --git a/src/modules/module-protocol-pulse/stream.c b/src/modules/module-protocol-pulse/stream.c
+index 4b223b878..484a12a05 100644
+--- a/src/modules/module-protocol-pulse/stream.c
++++ b/src/modules/module-protocol-pulse/stream.c
+@@ -24,6 +24,7 @@
+ #include "internal.h"
+ #include "log.h"
+ #include "message.h"
++#include "modules/pulse-module-compress-offload-forwarder.h"
+ #include "reply.h"
+ #include "stream.h"
+ 
+@@ -148,6 +149,10 @@ void stream_free(struct stream *stream)
+ 	if (stream->killed)
+ 		stream_send_killed(stream);
+ 
++	if (stream->type == STREAM_TYPE_PLAYBACK && stream->compress_offload)
++		compress_offload_forwarder_teardown(
++				stream->compress_forwarder_source);
++
+ 	if (stream->stream) {
+ 		spa_hook_remove(&stream->stream_listener);
+ 		pw_stream_disconnect(stream->stream);
+diff --git a/src/modules/module-protocol-pulse/stream.h b/src/modules/module-protocol-pulse/stream.h
+index 8e1fe178c..d2ff2edaa 100644
+--- a/src/modules/module-protocol-pulse/stream.h
++++ b/src/modules/module-protocol-pulse/stream.h
+@@ -87,6 +87,7 @@ struct stream {
+ 	uint32_t frame_size;
+ 	uint32_t rate;
+ 	uint64_t lat_usec;
++	char compress_forwarder_source[128];
+ 
+ 	struct volume volume;
+ 	bool muted;
+@@ -107,6 +108,7 @@ struct stream {
+ 	unsigned int fail_on_suspend:1;
+ 	unsigned int is_suspended:1;
+ 	unsigned int dont_inhibit_auto_suspend:1;
++	unsigned int compress_offload:1;
+ };
+ 
+ struct stream *stream_new(struct client *client, enum stream_type type, uint32_t create_tag,
+-- 
+Gitlab
