diff --git a/apps/app_confbridge.c b/apps/app_confbridge.c
index d8407d857aa662b9d83564211015b3ce303c356f..25cf2758feda353932294dd8068c0d48a4926528 100644
--- a/apps/app_confbridge.c
+++ b/apps/app_confbridge.c
@@ -1544,6 +1544,13 @@ static struct confbridge_conference *join_conference_bridge(const char *conferen
 			ast_bridge_set_sfu_video_mode(conference->bridge);
 			ast_bridge_set_video_update_discard(conference->bridge, conference->b_profile.video_update_discard);
 			ast_bridge_set_remb_send_interval(conference->bridge, conference->b_profile.remb_send_interval);
+			if (ast_test_flag(&conference->b_profile, BRIDGE_OPT_REMB_BEHAVIOR_AVERAGE)) {
+				ast_brige_set_remb_behavior(conference->bridge, AST_BRIDGE_VIDEO_SFU_REMB_AVERAGE);
+			} else if (ast_test_flag(&conference->b_profile, BRIDGE_OPT_REMB_BEHAVIOR_LOWEST)) {
+				ast_brige_set_remb_behavior(conference->bridge, AST_BRIDGE_VIDEO_SFU_REMB_LOWEST);
+			} else if (ast_test_flag(&conference->b_profile, BRIDGE_OPT_REMB_BEHAVIOR_HIGHEST)) {
+				ast_brige_set_remb_behavior(conference->bridge, AST_BRIDGE_VIDEO_SFU_REMB_HIGHEST);
+			}
 		}
 
 		/* Link it into the conference bridges container */
diff --git a/apps/confbridge/conf_config_parser.c b/apps/confbridge/conf_config_parser.c
index f9d74831c02062d24ead97d32f28e9c6385aa149..c143e39e2d0c9291ddd762e49a1db4b8eca6c631 100644
--- a/apps/confbridge/conf_config_parser.c
+++ b/apps/confbridge/conf_config_parser.c
@@ -470,6 +470,27 @@
 						better quality for all receivers.
 					</para></description>
 				</configOption>
+				<configOption name="remb_behavior" default="average">
+					<synopsis>Sets how REMB reports are generated from multiple sources</synopsis>
+					<description><para>
+						Sets how REMB reports are combined from multiple sources to form one. A REMB report
+						consists of information about the receiver estimated maximum bitrate. As a source
+						stream may be forwarded to multiple receivers the reports must be combined into
+						a single one which is sent to the sender.</para>
+						<enumlist>
+							<enum name="average">
+								<para>The average of all estimated maximum bitrates is taken and sent
+								to the sender.</para>
+							</enum>
+							<enum name="lowest">
+								<para>The lowest estimated maximum bitrate is forwarded to the sender.</para>
+							</enum>
+							<enum name="highest">
+								<para>The highest estimated maximum bitrate is forwarded to the sender.</para>
+							</enum>
+						</enumlist>
+					</description>
+				</configOption>
 				<configOption name="template">
 					<synopsis>When using the CONFBRIDGE dialplan function, use a bridge profile as a template for creating a new temporary profile</synopsis>
 				</configOption>
@@ -1675,6 +1696,23 @@ static char *handle_cli_confbridge_show_bridge_profile(struct ast_cli_entry *e,
 	ast_cli(a->fd,"Video Update Discard: %u\n", b_profile.video_update_discard);
 	ast_cli(a->fd,"REMB Send Interval: %u\n", b_profile.remb_send_interval);
 
+	switch (b_profile.flags
+		& (BRIDGE_OPT_REMB_BEHAVIOR_AVERAGE | BRIDGE_OPT_REMB_BEHAVIOR_LOWEST
+			| BRIDGE_OPT_REMB_BEHAVIOR_HIGHEST)) {
+	case BRIDGE_OPT_REMB_BEHAVIOR_AVERAGE:
+		ast_cli(a->fd, "REMB Behavior:           average\n");
+		break;
+	case BRIDGE_OPT_REMB_BEHAVIOR_LOWEST:
+		ast_cli(a->fd, "REMB Behavior:           lowest\n");
+		break;
+	case BRIDGE_OPT_REMB_BEHAVIOR_HIGHEST:
+		ast_cli(a->fd, "REMB Behavior:           highest\n");
+		break;
+	default:
+		ast_assert(0);
+		break;
+	}
+
 	ast_cli(a->fd,"sound_only_person:    %s\n", conf_get_sound(CONF_SOUND_ONLY_PERSON, b_profile.sounds));
 	ast_cli(a->fd,"sound_only_one:       %s\n", conf_get_sound(CONF_SOUND_ONLY_ONE, b_profile.sounds));
 	ast_cli(a->fd,"sound_has_joined:     %s\n", conf_get_sound(CONF_SOUND_HAS_JOINED, b_profile.sounds));
@@ -2011,6 +2049,30 @@ static int video_mode_handler(const struct aco_option *opt, struct ast_variable
 	return 0;
 }
 
+static int remb_behavior_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
+{
+	struct bridge_profile *b_profile = obj;
+
+	if (strcasecmp(var->name, "remb_behavior")) {
+		return -1;
+	}
+
+	ast_clear_flag(b_profile, BRIDGE_OPT_REMB_BEHAVIOR_AVERAGE |
+		BRIDGE_OPT_REMB_BEHAVIOR_LOWEST |
+		BRIDGE_OPT_REMB_BEHAVIOR_HIGHEST);
+
+	if (!strcasecmp(var->value, "average")) {
+		ast_set_flag(b_profile, BRIDGE_OPT_REMB_BEHAVIOR_AVERAGE);
+	} else if (!strcasecmp(var->value, "lowest")) {
+		ast_set_flag(b_profile, BRIDGE_OPT_REMB_BEHAVIOR_LOWEST);
+	} else if (!strcasecmp(var->value, "highest")) {
+		ast_set_flag(b_profile, BRIDGE_OPT_REMB_BEHAVIOR_HIGHEST);
+	} else {
+		return -1;
+	}
+	return 0;
+}
+
 static int user_template_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
 {
 	struct user_profile *u_profile = obj;
@@ -2245,6 +2307,7 @@ int conf_load_config(void)
 	aco_option_register_custom(&cfg_info, "sound_", ACO_PREFIX, bridge_types, NULL, sound_option_handler, 0);
 	aco_option_register(&cfg_info, "video_update_discard", ACO_EXACT, bridge_types, "2000", OPT_UINT_T, 0, FLDSET(struct bridge_profile, video_update_discard));
 	aco_option_register(&cfg_info, "remb_send_interval", ACO_EXACT, bridge_types, "0", OPT_UINT_T, 0, FLDSET(struct bridge_profile, remb_send_interval));
+	aco_option_register_custom(&cfg_info, "remb_behavior", ACO_EXACT, bridge_types, "average", remb_behavior_handler, 0);
 	/* This option should only be used with the CONFBRIDGE dialplan function */
 	aco_option_register_custom(&cfg_info, "template", ACO_EXACT, bridge_types, NULL, bridge_template_handler, 0);
 
diff --git a/apps/confbridge/include/confbridge.h b/apps/confbridge/include/confbridge.h
index c2f8f9a585f728ecd067be5573323e9cf7c5f145..0a0a5713fa8264de04efcce44ad2a67ba6500c4c 100644
--- a/apps/confbridge/include/confbridge.h
+++ b/apps/confbridge/include/confbridge.h
@@ -76,6 +76,9 @@ enum bridge_profile_flags {
 	BRIDGE_OPT_RECORD_FILE_TIMESTAMP = (1 << 5), /*!< Set if the record file should have a timestamp appended */
 	BRIDGE_OPT_BINAURAL_ACTIVE = (1 << 6), /*!< Set if binaural convolution is activated */
 	BRIDGE_OPT_VIDEO_SRC_SFU = (1 << 7), /*!< Selective forwarding unit */
+	BRIDGE_OPT_REMB_BEHAVIOR_AVERAGE = (1 << 8), /*!< The average of all REMB reports is sent to the sender */
+	BRIDGE_OPT_REMB_BEHAVIOR_LOWEST = (1 << 9), /*!< The lowest estimated maximum bitrate is sent to the sender */
+	BRIDGE_OPT_REMB_BEHAVIOR_HIGHEST = (1 << 10), /*!< The highest estimated maximum bitrate is sent to the sender */
 };
 
 enum conf_menu_action_id {
diff --git a/bridges/bridge_softmix.c b/bridges/bridge_softmix.c
index 16e1fb897c19b3baa4e532eca49141f72118c805..f0a3fb42df45709f0f5c07ae87cec0b98412d528 100644
--- a/bridges/bridge_softmix.c
+++ b/bridges/bridge_softmix.c
@@ -69,6 +69,15 @@
 #define SOFTBRIDGE_VIDEO_DEST_LEN strlen(SOFTBRIDGE_VIDEO_DEST_PREFIX)
 #define SOFTBRIDGE_VIDEO_DEST_SEPARATOR '_'
 
+struct softmix_remb_collector {
+	/*! The frame which will be given to each source stream */
+	struct ast_frame frame;
+	/*! The REMB to send to the source which is collecting REMB reports */
+	struct ast_rtp_rtcp_feedback feedback;
+	/*! The maximum bitrate */
+	unsigned int bitrate;
+};
+
 struct softmix_stats {
 	/*! Each index represents a sample rate used above the internal rate. */
 	unsigned int sample_rates[16];
@@ -768,6 +777,10 @@ static void softmix_bridge_leave(struct ast_bridge *bridge, struct ast_bridge_ch
 
 	ast_stream_topology_free(sc->topology);
 
+	ao2_cleanup(sc->remb_collector);
+
+	AST_VECTOR_FREE(&sc->video_sources);
+
 	/* Drop mutex lock */
 	ast_mutex_destroy(&sc->lock);
 
@@ -1158,6 +1171,39 @@ static int softmix_bridge_write_control(struct ast_bridge *bridge, struct ast_br
 	return 0;
 }
 
+/*!
+ * \internal
+ * \brief Determine what to do with an RTCP frame.
+ * \since 15.4.0
+ *
+ * \param bridge Which bridge is getting the frame
+ * \param bridge_channel Which channel is writing the frame.
+ * \param frame What is being written.
+ */
+static void softmix_bridge_write_rtcp(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, struct ast_frame *frame)
+{
+	struct ast_rtp_rtcp_feedback *feedback = frame->data.ptr;
+	struct softmix_channel *sc = bridge_channel->tech_pvt;
+
+	/* We only care about REMB reports right now. In the future we may be able to use sender or
+	 * receiver reports to further tweak things, but not yet.
+	 */
+	if (frame->subclass.integer != AST_RTP_RTCP_PSFB || feedback->fmt != AST_RTP_RTCP_FMT_REMB ||
+		bridge->softmix.video_mode.mode != AST_BRIDGE_VIDEO_MODE_SFU ||
+		!bridge->softmix.video_mode.mode_data.sfu_data.remb_send_interval) {
+		return;
+	}
+
+	/* REMB is the total estimated maximum bitrate across all streams within the session, so we store
+	 * only the latest report and use it everywhere.
+	 */
+	ast_mutex_lock(&sc->lock);
+	sc->remb = feedback->remb;
+	ast_mutex_unlock(&sc->lock);
+
+	return;
+}
+
 /*!
  * \internal
  * \brief Determine what to do with a frame written into the bridge.
@@ -1204,6 +1250,9 @@ static int softmix_bridge_write(struct ast_bridge *bridge, struct ast_bridge_cha
 	case AST_FRAME_CONTROL:
 		res = softmix_bridge_write_control(bridge, bridge_channel, frame);
 		break;
+	case AST_FRAME_RTCP:
+		softmix_bridge_write_rtcp(bridge, bridge_channel, frame);
+		break;
 	case AST_FRAME_BRIDGE_ACTION:
 		res = ast_bridge_queue_everyone_else(bridge, bridge_channel, frame);
 		break;
@@ -1219,6 +1268,104 @@ static int softmix_bridge_write(struct ast_bridge *bridge, struct ast_bridge_cha
 	return res;
 }
 
+static void remb_collect_report(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel,
+	struct softmix_bridge_data *softmix_data, struct softmix_channel *sc)
+{
+	int i;
+	unsigned int bitrate;
+
+	/* If there are no video sources that we are a receiver of then we have noone to
+	 * report REMB to.
+	 */
+	if (!AST_VECTOR_SIZE(&sc->video_sources)) {
+		return;
+	}
+
+	/* We evenly divide the available maximum bitrate across the video sources
+	 * to this receiver so each source gets an equal slice.
+	 */
+	bitrate = (sc->remb.br_mantissa << sc->remb.br_exp) / AST_VECTOR_SIZE(&sc->video_sources);
+
+	/* If this receiver has no bitrate yet ignore it */
+	if (!bitrate) {
+		return;
+	}
+
+	for (i = 0; i < AST_VECTOR_SIZE(&sc->video_sources); ++i) {
+		struct softmix_remb_collector *collector;
+
+		/* The collector will always exist if a video source is in our list */
+		collector = AST_VECTOR_GET(&softmix_data->remb_collectors, AST_VECTOR_GET(&sc->video_sources, i));
+
+		if (!collector->bitrate) {
+			collector->bitrate = bitrate;
+			continue;
+		}
+
+		switch (bridge->softmix.video_mode.mode_data.sfu_data.remb_behavior) {
+		case AST_BRIDGE_VIDEO_SFU_REMB_AVERAGE:
+			collector->bitrate = (collector->bitrate + bitrate) / 2;
+			break;
+		case AST_BRIDGE_VIDEO_SFU_REMB_LOWEST:
+			if (bitrate < collector->bitrate) {
+				collector->bitrate = bitrate;
+			}
+			break;
+		case AST_BRIDGE_VIDEO_SFU_REMB_HIGHEST:
+			if (bitrate > collector->bitrate) {
+				collector->bitrate = bitrate;
+			}
+			break;
+		}
+	}
+}
+
+static void remb_send_report(struct ast_bridge_channel *bridge_channel, struct softmix_channel *sc)
+{
+	int i;
+
+	if (!sc->remb_collector) {
+		return;
+	}
+
+	/* If we have a new bitrate then use it for the REMB, if not we use the previous
+	 * one until we know otherwise. This way the bitrate doesn't drop to 0 all of a sudden.
+	 */
+	if (sc->remb_collector->bitrate) {
+		sc->remb_collector->feedback.remb.br_mantissa = sc->remb_collector->bitrate;
+		sc->remb_collector->feedback.remb.br_exp = 0;
+
+		/* The mantissa only has 18 bits available, so while it exceeds them we bump
+		 * up the exp.
+		 */
+		while (sc->remb_collector->feedback.remb.br_mantissa > 0x3ffff) {
+			sc->remb_collector->feedback.remb.br_mantissa = sc->remb_collector->feedback.remb.br_mantissa >> 1;
+			sc->remb_collector->feedback.remb.br_exp++;
+		}
+	}
+
+	for (i = 0; i < AST_VECTOR_SIZE(&bridge_channel->stream_map.to_bridge); ++i) {
+		int bridge_num = AST_VECTOR_GET(&bridge_channel->stream_map.to_bridge, i);
+
+		/* If this stream is not being provided to the bridge there can be no receivers of it
+		 * so therefore no REMB reports.
+		 */
+		if (bridge_num == -1) {
+			continue;
+		}
+
+		/* We need to update the frame with this stream, or else it won't be
+		 * properly routed. We don't use the actual channel stream identifier as
+		 * the bridging core will do the translation from bridge stream identifier to
+		 * channel stream identifier.
+		 */
+		sc->remb_collector->frame.stream_num = bridge_num;
+		ast_bridge_channel_queue_frame(bridge_channel, &sc->remb_collector->frame);
+	}
+
+	sc->remb_collector->bitrate = 0;
+}
+
 static void gather_softmix_stats(struct softmix_stats *stats,
 	const struct softmix_bridge_data *softmix_data,
 	struct ast_bridge_channel *bridge_channel)
@@ -1440,6 +1587,7 @@ static int softmix_mixing_loop(struct ast_bridge *bridge)
 		struct ast_format *cur_slin = ast_format_cache_get_slin_by_rate(softmix_data->internal_rate);
 		unsigned int softmix_samples = SOFTMIX_SAMPLES(softmix_data->internal_rate, softmix_data->internal_mixing_interval);
 		unsigned int softmix_datalen = SOFTMIX_DATALEN(softmix_data->internal_rate, softmix_data->internal_mixing_interval);
+		int remb_update = 0;
 
 		if (softmix_datalen > MAX_DATALEN) {
 			/* This should NEVER happen, but if it does we need to know about it. Almost
@@ -1478,6 +1626,14 @@ static int softmix_mixing_loop(struct ast_bridge *bridge)
 		check_binaural_position_change(bridge, softmix_data);
 #endif
 
+		/* If we need to do a REMB update to all video sources then do so */
+		if (bridge->softmix.video_mode.mode == AST_BRIDGE_VIDEO_MODE_SFU &&
+			bridge->softmix.video_mode.mode_data.sfu_data.remb_send_interval &&
+			ast_tvdiff_ms(ast_tvnow(), softmix_data->last_remb_update) > bridge->softmix.video_mode.mode_data.sfu_data.remb_send_interval) {
+			remb_update = 1;
+			softmix_data->last_remb_update = ast_tvnow();
+		}
+
 		/* Go through pulling audio from each factory that has it available */
 		AST_LIST_TRAVERSE(&bridge->channels, bridge_channel, entry) {
 			struct softmix_channel *sc = bridge_channel->tech_pvt;
@@ -1512,6 +1668,9 @@ static int softmix_mixing_loop(struct ast_bridge *bridge)
 #endif
 				mixing_array.used_entries++;
 			}
+			if (remb_update) {
+				remb_collect_report(bridge, bridge_channel, softmix_data, sc);
+			}
 			ast_mutex_unlock(&sc->lock);
 		}
 
@@ -1562,6 +1721,10 @@ static int softmix_mixing_loop(struct ast_bridge *bridge)
 
 			/* A frame is now ready for the channel. */
 			ast_bridge_channel_queue_frame(bridge_channel, &sc->write_frame);
+
+			if (remb_update) {
+				remb_send_report(bridge_channel, sc);
+			}
 		}
 
 		update_all_rates = 0;
@@ -1688,6 +1851,8 @@ static void softmix_bridge_data_destroy(struct softmix_bridge_data *softmix_data
 	}
 	ast_mutex_destroy(&softmix_data->lock);
 	ast_cond_destroy(&softmix_data->cond);
+	AST_VECTOR_RESET(&softmix_data->remb_collectors, ao2_cleanup);
+	AST_VECTOR_FREE(&softmix_data->remb_collectors);
 	ast_free(softmix_data);
 }
 
@@ -1718,6 +1883,8 @@ static int softmix_bridge_create(struct ast_bridge *bridge)
 			softmix_data->internal_mixing_interval);
 #endif
 
+	AST_VECTOR_INIT(&softmix_data->remb_collectors, 0);
+
 	bridge->tech_pvt = softmix_data;
 
 	/* Start the mixing thread. */
@@ -1814,7 +1981,10 @@ static void map_source_to_destinations(const char *source_stream_name, const cha
 
 			stream = ast_stream_topology_get_stream(topology, i);
 			if (is_video_dest(stream, source_channel_name, source_stream_name)) {
+				struct softmix_channel *sc = participant->tech_pvt;
+
 				AST_VECTOR_REPLACE(&participant->stream_map.to_channel, bridge_stream_position, i);
+				AST_VECTOR_APPEND(&sc->video_sources, bridge_stream_position);
 				break;
 			}
 		}
@@ -1823,6 +1993,58 @@ static void map_source_to_destinations(const char *source_stream_name, const cha
 	}
 }
 
+/*!
+ * \brief Allocate a REMB collector
+ *
+ * \retval non-NULL success
+ * \retval NULL failure
+ */
+static struct softmix_remb_collector *remb_collector_alloc(void)
+{
+	struct softmix_remb_collector *collector;
+
+	collector = ao2_alloc_options(sizeof(*collector), NULL, AO2_ALLOC_OPT_LOCK_NOLOCK);
+	if (!collector) {
+		return NULL;
+	}
+
+	collector->frame.frametype = AST_FRAME_RTCP;
+	collector->frame.subclass.integer = AST_RTP_RTCP_PSFB;
+	collector->feedback.fmt = AST_RTP_RTCP_FMT_REMB;
+	collector->frame.data.ptr = &collector->feedback;
+	collector->frame.datalen = sizeof(collector->feedback);
+
+	return collector;
+}
+
+/*!
+ * \brief Setup REMB collection for a particular bridge stream and channel.
+ *
+ * \param bridge The bridge
+ * \param bridge_channel Channel that is collecting REMB information
+ * \param bridge_stream_position The slot in the bridge where source video comes from
+ */
+static void remb_enable_collection(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel,
+	size_t bridge_stream_position)
+{
+	struct softmix_channel *sc = bridge_channel->tech_pvt;
+	struct softmix_bridge_data *softmix_data = bridge->tech_pvt;
+
+	if (!sc->remb_collector) {
+		sc->remb_collector = remb_collector_alloc();
+		if (!sc->remb_collector) {
+			/* This is not fatal. Things will still continue to work but we won't
+			 * produce a REMB report to the sender.
+			 */
+			return;
+		}
+	}
+
+	if (AST_VECTOR_REPLACE(&softmix_data->remb_collectors, bridge_stream_position, ao2_bump(sc->remb_collector))) {
+		ao2_ref(sc->remb_collector, -1);
+	}
+}
+
 /*!
  * \brief stream_topology_changed callback
  *
@@ -1835,6 +2057,8 @@ static void map_source_to_destinations(const char *source_stream_name, const cha
  */
 static void softmix_bridge_stream_topology_changed(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel)
 {
+	struct softmix_bridge_data *softmix_data = bridge->tech_pvt;
+	struct softmix_channel *sc;
 	struct ast_bridge_channel *participant;
 	struct ast_vector_int media_types;
 	int nths[AST_MEDIA_TYPE_END] = {0};
@@ -1852,11 +2076,22 @@ static void softmix_bridge_stream_topology_changed(struct ast_bridge *bridge, st
 
 	AST_VECTOR_INIT(&media_types, AST_MEDIA_TYPE_END);
 
+	/* The bridge stream identifiers may change, so reset the mapping for them.
+	 * When channels end up getting added back in they'll reuse their existing
+	 * collector and won't need to allocate a new one (unless they were just added).
+	 */
+	AST_VECTOR_RESET(&softmix_data->remb_collectors, ao2_cleanup);
+
 	/* First traversal: re-initialize all of the participants' stream maps */
 	AST_LIST_TRAVERSE(&bridge->channels, participant, entry) {
 		ast_bridge_channel_lock(participant);
+
 		AST_VECTOR_RESET(&participant->stream_map.to_channel, AST_VECTOR_ELEM_CLEANUP_NOOP);
 		AST_VECTOR_RESET(&participant->stream_map.to_bridge, AST_VECTOR_ELEM_CLEANUP_NOOP);
+
+		sc = participant->tech_pvt;
+		AST_VECTOR_RESET(&sc->video_sources, AST_VECTOR_ELEM_CLEANUP_NOOP);
+
 		ast_bridge_channel_unlock(participant);
 	}
 
@@ -1897,7 +2132,12 @@ static void softmix_bridge_stream_topology_changed(struct ast_bridge *bridge, st
 			if (is_video_source(stream)) {
 				AST_VECTOR_APPEND(&media_types, AST_MEDIA_TYPE_VIDEO);
 				AST_VECTOR_REPLACE(&participant->stream_map.to_bridge, i, AST_VECTOR_SIZE(&media_types) - 1);
-				AST_VECTOR_REPLACE(&participant->stream_map.to_channel, AST_VECTOR_SIZE(&media_types) - 1, -1);
+				/*
+				 * There are cases where we need to bidirectionally send frames, such as for REMB reports
+				 * so we also map back to the channel.
+				 */
+				AST_VECTOR_REPLACE(&participant->stream_map.to_channel, AST_VECTOR_SIZE(&media_types) - 1, i);
+				remb_enable_collection(bridge, participant, AST_VECTOR_SIZE(&media_types) - 1);
 				/*
 				 * Unlock the channel and participant to prevent
 				 * potential deadlock in map_source_to_destinations().
diff --git a/bridges/bridge_softmix/include/bridge_softmix_internal.h b/bridges/bridge_softmix/include/bridge_softmix_internal.h
index f842acb5e1f4022c7ce5a82b072cb0c7481ff79e..3aa90915defd4099b34ec287a4603d92d7a42f6c 100644
--- a/bridges/bridge_softmix/include/bridge_softmix_internal.h
+++ b/bridges/bridge_softmix/include/bridge_softmix_internal.h
@@ -50,6 +50,8 @@
 #include "asterisk/astobj2.h"
 #include "asterisk/timing.h"
 #include "asterisk/translate.h"
+#include "asterisk/rtp_engine.h"
+#include "asterisk/vector.h"
 
 #ifdef BINAURAL_RENDERING
 #include <fftw3.h>
@@ -124,6 +126,8 @@ struct video_follow_talker_data {
 	int energy_average;
 };
 
+struct softmix_remb_collector;
+
 /*! \brief Structure which contains per-channel mixing information */
 struct softmix_channel {
 	/*! Lock to protect this structure */
@@ -169,6 +173,12 @@ struct softmix_channel {
 	struct video_follow_talker_data video_talker;
 	/*! The ideal stream topology for the channel */
 	struct ast_stream_topology *topology;
+	/*! The latest REMB report from this participant */
+	struct ast_rtp_rtcp_feedback_remb remb;
+	/*! The REMB collector for this channel, collects REMB from all video receivers */
+	struct softmix_remb_collector *remb_collector;
+	/*! The bridge streams which are feeding us video sources */
+	AST_VECTOR(, int) video_sources;
 };
 
 struct softmix_bridge_data {
@@ -202,6 +212,10 @@ struct softmix_bridge_data {
 	unsigned int binaural_init;
 	/*! The last time a video update was sent into the bridge */
 	struct timeval last_video_update;
+	/*! The last time a REMB frame was sent to each source of video */
+	struct timeval last_remb_update;
+	/*! Per-bridge stream REMB collectors, which flow back to video source */
+	AST_VECTOR(, struct softmix_remb_collector *) remb_collectors;
 };
 
 struct softmix_mixing_array {
diff --git a/configs/samples/confbridge.conf.sample b/configs/samples/confbridge.conf.sample
index 4028593d2fbcb545e771aded50c16f29eecfdf35..8b276cdb8013b2b2c55696ed7646df8a8ddc19ec 100644
--- a/configs/samples/confbridge.conf.sample
+++ b/configs/samples/confbridge.conf.sample
@@ -239,6 +239,10 @@ type=bridge
                            ; A REMB frame contains receiver estimated maximum bitrate information. By creating a combined
                            ; frame and sending it to the sources of video the sender can be influenced on what bitrate
                            ; they choose allowing a better experience for the receivers. This defaults to 0, or disabled.
+;remb_behavior=average     ; How the combined REMB report for an SFU video bridge is constructed. If set to "average" then
+                           ; the estimated maximum bitrate of each receiver is used to construct an average bitrate. If
+                           ; set to "lowest" the lowest maximum bitrate is forwarded to the sender. If set to "highest"
+                           ; the highest maximum bitrate is forwarded to the sender. This defaults to "average".
 
 ; All sounds in the conference are customizable using the bridge profile options below.
 ; Simply state the option followed by the filename or full path of the filename after
diff --git a/include/asterisk/bridge.h b/include/asterisk/bridge.h
index c96cefb6047c975d87e37767639bd7eadad563c4..3584085affa942f08b8d9659b6e7a70b50e87087 100644
--- a/include/asterisk/bridge.h
+++ b/include/asterisk/bridge.h
@@ -126,6 +126,24 @@ struct ast_bridge_video_talker_src_data {
 	struct ast_channel *chan_old_vsrc;
 };
 
+/*! \brief REMB report behaviors */
+enum ast_bridge_video_sfu_remb_behavior {
+	/*! The average of all reports is sent to the sender */
+	AST_BRIDGE_VIDEO_SFU_REMB_AVERAGE = 0,
+	/*! The lowest reported bitrate is forwarded to the sender */
+	AST_BRIDGE_VIDEO_SFU_REMB_LOWEST,
+	/*! The highest reported bitrate is forwarded to the sender */
+	AST_BRIDGE_VIDEO_SFU_REMB_HIGHEST,
+};
+
+/*! \brief This is used for selective forwarding unit configuration */
+struct ast_bridge_video_sfu_data {
+	/*! The interval at which a REMB report is generated and sent */
+	unsigned int remb_send_interval;
+	/*! How the combined REMB report is generated */
+	enum ast_bridge_video_sfu_remb_behavior remb_behavior;
+};
+
 /*! \brief Data structure that defines a video source mode */
 struct ast_bridge_video_mode {
 	enum ast_bridge_video_mode_type mode;
@@ -133,9 +151,10 @@ struct ast_bridge_video_mode {
 	union {
 		struct ast_bridge_video_single_src_data single_src_data;
 		struct ast_bridge_video_talker_src_data talker_src_data;
+		struct ast_bridge_video_sfu_data sfu_data;
 	} mode_data;
+	/*! The minimum interval between video updates */
 	unsigned int video_update_discard;
-	unsigned int remb_send_interval;
 };
 
 /*!
@@ -917,9 +936,21 @@ void ast_bridge_set_video_update_discard(struct ast_bridge *bridge, unsigned int
  *
  * \param bridge Bridge to set the REMB send interval on
  * \param remb_send_interval The REMB send interval
+ *
+ * \note This can only be called when the bridge has been set to the SFU video mode.
  */
 void ast_bridge_set_remb_send_interval(struct ast_bridge *bridge, unsigned int remb_send_interval);
 
+/*!
+ * \brief Set the REMB report generation behavior on a bridge
+ *
+ * \param bridge Bridge to set the REMB behavior on
+ * \param behavior How REMB reports are generated
+ *
+ * \note This can only be called when the bridge has been set to the SFU video mode.
+ */
+void ast_brige_set_remb_behavior(struct ast_bridge *bridge, enum ast_bridge_video_sfu_remb_behavior behavior);
+
 /*!
  * \brief Update information about talker energy for talker src video mode.
  */
diff --git a/main/bridge.c b/main/bridge.c
index 8795081e2b307d1439c54889de8d3f6275a175df..2b347fd3fbc7ed4e9ed642d77026620e4151c4ef 100644
--- a/main/bridge.c
+++ b/main/bridge.c
@@ -3852,8 +3852,19 @@ void ast_bridge_set_video_update_discard(struct ast_bridge *bridge, unsigned int
 
 void ast_bridge_set_remb_send_interval(struct ast_bridge *bridge, unsigned int remb_send_interval)
 {
+	ast_assert(bridge->softmix.video_mode.mode == AST_BRIDGE_VIDEO_MODE_SFU);
+
+	ast_bridge_lock(bridge);
+	bridge->softmix.video_mode.mode_data.sfu_data.remb_send_interval = remb_send_interval;
+	ast_bridge_unlock(bridge);
+}
+
+void ast_brige_set_remb_behavior(struct ast_bridge *bridge, enum ast_bridge_video_sfu_remb_behavior behavior)
+{
+	ast_assert(bridge->softmix.video_mode.mode == AST_BRIDGE_VIDEO_MODE_SFU);
+
 	ast_bridge_lock(bridge);
-	bridge->softmix.video_mode.remb_send_interval = remb_send_interval;
+	bridge->softmix.video_mode.mode_data.sfu_data.remb_behavior = behavior;
 	ast_bridge_unlock(bridge);
 }