diff --git a/README.md b/README.md
index ab723bd87..54e4e2d85 100644
--- a/README.md
+++ b/README.md
@@ -658,6 +658,13 @@ The request dictionary must contain at least the following keys:
 
 Optionally included keys are:
 
+* `from-tags`
+
+	Contains a list of strings used to selected multiple existing call
+	participants (e.g. for the `subscribe request` message). An alternative
+	way to list multiple tags is by putting them into the `flags` list,
+	each prefixed with `from-tags-`.
+
 * `via-branch`
 
 	The SIP `Via` branch as string. Used to additionally refine the matching logic between media streams
@@ -2095,24 +2102,30 @@ described above.
 ---------------------------
 
 This message is used to request subscription (i.e. receiving a copy of the
-media) to an existing call participant, which must have been created either
-through the offer/answer mechanism, or through the publish mechanism.
+media) to one or multiple existing call participants, which must have been
+created either through the offer/answer mechanism, or through the publish
+mechanism.
+
+A single call participant can be selected in the same way as described under
+`block DTMF`. Multiple call participants can be selected either by using the
+`all` keyword, in which case all call participants that were created through
+the offer/answer mechanism will be selected, or by providing a list of tags
+(from-tags) in the `from-tags` list.
 
-The call participant is selected in the same way as described under `block
-DTMF` except that one call participant must be selected (i.e. the `all` keyword
-cannot be used). This message then creates a new call participant, which
-corresponds to the subscription. This new call participant will be identified
-by a newly generated unique tag, or by the tag given in the `to-tag` key. If a
-label is to be set for the newly created subscription, it can be set through
-`set-label`.
+This message then creates a new call participant, which corresponds to the
+subscription. This new call participant will be identified by a newly generated
+unique tag, or by the tag given in the `to-tag` key. If a label is to be set
+for the newly created subscription, it can be set through `set-label`.
 
 The reply message will contain a sendonly offer SDP in `sdp` which by default
-will mirror the SDP of the call participant being subscribed to. This offer SDP
-can be manipulated with the same flags as used in an `offer` message, including
-the option to manipulate the codecs. The reply message will also contain the
-`from-tag` (corresponding to the call participant being subscribed to) and the
-`to-tag` (corresponding to the subscription, either generated or taken from the
-received message).
+will mirror the SDP of the call participant being subscribed to. If multiple
+call participants are subscribed to at the same time, then this SDP will
+contain multiple media sections, combined out of the media sections of all
+selected call participants. This offer SDP can be manipulated with the same
+flags as used in an `offer` message, including the option to manipulate the
+codecs. The reply message will also contain the `from-tags` (corresponding to
+the call participants being subscribed to) and the `to-tag` (corresponding to
+the subscription, either generated or taken from the received message).
 
 If a `subscribe request` is made for an existing `to-tag` then all existing
 subscriptions for that `to-tag` are deleted before the new subscriptions are
diff --git a/daemon/call.c b/daemon/call.c
index 49606f81d..5656a1ada 100644
--- a/daemon/call.c
+++ b/daemon/call.c
@@ -825,11 +825,18 @@ struct call_media *call_media_new(struct call *call) {
 }
 
 static struct call_media *__get_media(struct call_monologue *ml, GList **it, const struct stream_params *sp,
-		const struct sdp_ng_flags *flags)
+		const struct sdp_ng_flags *flags, int index)
 {
 	struct call_media *med;
 	struct call *call;
 
+	// is this a repeated call with *it set but for a different ml?
+	if (*it) {
+		med = (*it)->data;
+		if (med->monologue != ml)
+			*it = NULL;
+	}
+
 	/* iterator points to last seen element, or NULL if uninitialized */
 	if (!*it)
 		*it = ml->medias.head;
@@ -849,21 +856,25 @@ static struct call_media *__get_media(struct call_monologue *ml, GList **it, con
 				STR_FMT(&sp->media_id));
 	}
 
+	unsigned int want_index = sp->index;
+	if (index != -1)
+		want_index = index;
+
 	/* possible incremental update, hunt for correct media struct */
 	while (*it) {
 		med = (*it)->data;
-		if (med->index == sp->index) {
-			__C_DBG("found existing call_media for stream #%u", sp->index);
+		if (med->index == want_index) {
+			__C_DBG("found existing call_media for stream #%u", want_index);
 			return med;
 		}
 		*it = (*it)->next;
 	}
 
-	__C_DBG("allocating new call_media for stream #%u", sp->index);
+	__C_DBG("allocating new call_media for stream #%u", want_index);
 	call = ml->call;
 	med = call_media_new(call);
 	med->monologue = ml;
-	med->index = sp->index;
+	med->index = want_index;
 	call_str_cpy(ml->call, &med->type, &sp->type);
 	med->type_id = sp->type_id;
 
@@ -1309,6 +1320,11 @@ static int __init_streams(struct call_media *A, struct call_media *B, const stru
 	la = A->streams.head;
 	lb = B ? B->streams.head : NULL;
 
+	if (B)
+		__C_DBG("Sink init media %u -> %u", A->index, B->index);
+	else
+		__C_DBG("Stream init media %u", A->index);
+
 	while (la) {
 		a = la->data;
 		b = lb ? lb->data : NULL;
@@ -2397,7 +2413,11 @@ static void __update_init_subscribers(struct call_monologue *ml, GQueue *streams
 	for (GList *l = ml->subscribers.head; l; l = l->next) {
 		struct call_subscription *cs = l->data;
 		struct call_monologue *sub_ml = cs->monologue;
-		sub_medias[num_subs++] = sub_ml->medias.head;
+		sub_medias[num_subs] = sub_ml->medias.head;
+		// skip into correct media section for multi-ml subscriptions
+		for (unsigned int offset = cs->media_offset; offset && sub_medias[num_subs]; offset--)
+			sub_medias[num_subs] = sub_medias[num_subs]->next;
+		num_subs++;
 	}
 	// keep num_subs as shortcut to ml->subscribers.length
 
@@ -2622,8 +2642,8 @@ int monologue_offer_answer(struct call_monologue *dialogue[2], GQueue *streams,
 
 		/* first, check for existence of call_media struct on both sides of
 		 * the dialogue */
-		media = __get_media(monologue, &ml_media, sp, flags);
-		other_media = __get_media(other_ml, &other_ml_media, sp, flags);
+		media = __get_media(monologue, &ml_media, sp, flags, -1);
+		other_media = __get_media(other_ml, &other_ml_media, sp, flags, -1);
 		/* OTHER is the side which has sent the message. SDP parameters in
 		 * "sp" are as advertised by OTHER side. The message will be sent to
 		 * THIS side. Parameters sent to THIS side may be overridden by
@@ -2742,6 +2762,10 @@ error_intf:
 }
 
 
+void call_subscriptions_clear(GQueue *q) {
+	g_queue_clear_full(q, call_subscription_free);
+}
+
 static void __unsubscribe_one_link(struct call_monologue *which, GList *which_cs_link) {
 	struct call_subscription *cs = which_cs_link->data;
 	struct call_subscription *rev_cs = cs->link->data;
@@ -2786,7 +2810,9 @@ static void __unsubscribe_from_all(struct call_monologue *ml) {
 		l = next;
 	}
 }
-void __add_subscription(struct call_monologue *which, struct call_monologue *to, bool offer_answer) {
+void __add_subscription(struct call_monologue *which, struct call_monologue *to, bool offer_answer,
+		unsigned int offset)
+{
 	if (g_hash_table_lookup(which->subscriptions_ht, to)) {
 		ilog(LOG_DEBUG, "Tag '" STR_FORMAT_M "' is already subscribed to '" STR_FORMAT_M "'",
 				STR_FMT_M(&which->tag),
@@ -2800,6 +2826,8 @@ void __add_subscription(struct call_monologue *which, struct call_monologue *to,
 	struct call_subscription *to_rev_cs = g_slice_alloc0(sizeof(*to_rev_cs));
 	which_cs->monologue = to;
 	to_rev_cs->monologue = which;
+	which_cs->media_offset = offset;
+	to_rev_cs->media_offset = offset;
 	// keep offer-answer subscriptions first in the list
 	if (!offer_answer) {
 		g_queue_push_tail(&which->subscriptions, which_cs);
@@ -2821,8 +2849,8 @@ void __add_subscription(struct call_monologue *which, struct call_monologue *to,
 static void __subscribe_offer_answer_both_ways(struct call_monologue *a, struct call_monologue *b) {
 	__unsubscribe_all_offer_answer_subscribers(a);
 	__unsubscribe_all_offer_answer_subscribers(b);
-	__add_subscription(a, b, true);
-	__add_subscription(b, a, true);
+	__add_subscription(a, b, true, 0);
+	__add_subscription(b, a, true, 0);
 }
 
 
@@ -2835,7 +2863,7 @@ int monologue_publish(struct call_monologue *ml, GQueue *streams, struct sdp_ng_
 
 	for (GList *l = streams->head; l; l = l->next) {
 		struct stream_params *sp = l->data;
-		struct call_media *media = __get_media(ml, &media_iter, sp, flags);
+		struct call_media *media = __get_media(ml, &media_iter, sp, flags, -1);
 
 		__media_init_from_flags(media, NULL, sp, flags);
 
@@ -2889,21 +2917,20 @@ int monologue_publish(struct call_monologue *ml, GQueue *streams, struct sdp_ng_
 }
 
 /* called with call->master_lock held in W */
-int monologue_subscribe_request(struct call_monologue *src_ml, struct call_monologue *dst_ml,
-		struct sdp_ng_flags *flags)
+static int monologue_subscribe_request1(struct call_monologue *src_ml, struct call_monologue *dst_ml,
+		struct sdp_ng_flags *flags, GList **src_media_it, GList **dst_media_it, unsigned int *index)
 {
-	__unsubscribe_from_all(dst_ml);
-
-	__call_monologue_init_from_flags(dst_ml, flags);
-
-	GList *dst_media_it = NULL;
-	GList *src_media_it = NULL;
+	unsigned int idx_diff = 0;
 
 	for (GList *l = src_ml->last_in_sdp_streams.head; l; l = l->next) {
 		struct stream_params *sp = l->data;
 
-		struct call_media *dst_media = __get_media(dst_ml, &dst_media_it, sp, flags);
-		struct call_media *src_media = __get_media(src_ml, &src_media_it, sp, flags);
+		struct call_media *dst_media = __get_media(dst_ml, dst_media_it, sp, flags, (*index)++);
+		struct call_media *src_media = __get_media(src_ml, src_media_it, sp, flags, -1);
+
+		// track media index difference if one ml is subscribed to multiple other mls
+		if (idx_diff == 0 && dst_media->index > src_media->index)
+			idx_diff = dst_media->index - src_media->index;
 
 		if (__media_init_from_flags(src_media, dst_media, sp, flags) == 1)
 			continue;
@@ -2942,7 +2969,7 @@ int monologue_subscribe_request(struct call_monologue *src_ml, struct call_monol
 			return -1;
 	}
 
-	__add_subscription(dst_ml, src_ml, false);
+	__add_subscription(dst_ml, src_ml, false, idx_diff);
 
 	__update_init_subscribers(src_ml, NULL, NULL);
 	__update_init_subscribers(dst_ml, NULL, NULL);
@@ -2950,20 +2977,54 @@ int monologue_subscribe_request(struct call_monologue *src_ml, struct call_monol
 	return 0;
 }
 /* called with call->master_lock held in W */
-int monologue_subscribe_answer(struct call_monologue *dst_ml, struct sdp_ng_flags *flags, GQueue *streams) {
-	if (!dst_ml->subscriptions.length)
-		return -1;
-	struct call_subscription *cs = dst_ml->subscriptions.head->data;
-	struct call_monologue *src_ml = cs->monologue;
+int monologue_subscribe_request(const GQueue *srcs, struct call_monologue *dst_ml,
+		struct sdp_ng_flags *flags)
+{
+	__unsubscribe_from_all(dst_ml);
+
+	__call_monologue_init_from_flags(dst_ml, flags);
 
 	GList *dst_media_it = NULL;
 	GList *src_media_it = NULL;
+	unsigned int index = 1; // running counter for output/dst medias
+
+	for (GList *sl = srcs->head; sl; sl = sl->next) {
+		struct call_subscription *cs = sl->data;
+		struct call_monologue *src_ml = cs->monologue;
+
+		int ret = monologue_subscribe_request1(src_ml, dst_ml, flags, &src_media_it, &dst_media_it,
+				&index);
+		if (ret)
+			return -1;
+	}
+	return 0;
+}
+
+/* called with call->master_lock held in W */
+int monologue_subscribe_answer(struct call_monologue *dst_ml, struct sdp_ng_flags *flags, GQueue *streams) {
+	GList *dst_media_it = NULL;
+	GList *src_media_it = NULL;
+	GList *src_ml_it = dst_ml->subscriptions.head;
+	unsigned int index = 1; // running counter for input/src medias
 
 	for (GList *l = streams->head; l; l = l->next) {
 		struct stream_params *sp = l->data;
 
-		struct call_media *dst_media = __get_media(dst_ml, &dst_media_it, sp, flags);
-		struct call_media *src_media = __get_media(src_ml, &src_media_it, sp, flags);
+		// grab the matching source ml:
+		// we need to move to the next one when we've reached the last media of
+		// the current source ml
+		if (src_media_it && !src_media_it->next) {
+			src_ml_it = src_ml_it->next;
+			index = 1; // starts over at 1
+		}
+		if (!src_ml_it)
+			return -1;
+
+		struct call_subscription *cs = src_ml_it->data;
+		struct call_monologue *src_ml = cs->monologue;
+
+		struct call_media *dst_media = __get_media(dst_ml, &dst_media_it, sp, flags, -1);
+		struct call_media *src_media = __get_media(src_ml, &src_media_it, sp, flags, index++);
 
 		if (__media_init_from_flags(dst_media, NULL, sp, flags) == 1)
 			continue;
@@ -2995,11 +3056,15 @@ int monologue_subscribe_answer(struct call_monologue *dst_ml, struct sdp_ng_flag
 	}
 
 	__update_init_subscribers(dst_ml, streams, flags);
-	__update_init_subscribers(src_ml, NULL, NULL);
-
-	dialogue_unkernelize(src_ml);
 	dialogue_unkernelize(dst_ml);
 
+	for (GList *l = dst_ml->subscriptions.head; l; l = l->next) {
+		struct call_subscription *cs = l->data;
+		struct call_monologue *src_ml = cs->monologue;
+		__update_init_subscribers(src_ml, NULL, NULL);
+		dialogue_unkernelize(src_ml);
+	}
+
 	return 0;
 }
 
diff --git a/daemon/call_interfaces.c b/daemon/call_interfaces.c
index 126cf3433..76df9432d 100644
--- a/daemon/call_interfaces.c
+++ b/daemon/call_interfaces.c
@@ -924,6 +924,9 @@ static void call_ng_flags_flags(struct sdp_ng_flags *out, str *s, void *dummy) {
 			break;
 		default:
 			// handle values aliases from other dictionaries
+			if (call_ng_flags_prefix(out, s, "from-tags-", call_ng_flags_esc_str_list,
+						&out->from_tags))
+				return;
 			if (call_ng_flags_prefix(out, s, "SDES-no-", call_ng_flags_str_ht, &out->sdes_no))
 				return;
 			if (call_ng_flags_prefix(out, s, "SDES-", ng_sdes_option, NULL))
@@ -996,6 +999,7 @@ static void call_ng_process_flags(struct sdp_ng_flags *out, bencode_item_t *inpu
 
 	bencode_get_alt(input, "call-id", "call-ID", &out->call_id);
 	bencode_dictionary_get_str(input, "from-tag", &out->from_tag);
+	call_ng_flags_list(out, input, "from-tags", call_ng_flags_esc_str_list, &out->from_tags);
 	bencode_dictionary_get_str(input, "to-tag", &out->to_tag);
 	bencode_dictionary_get_str(input, "via-branch", &out->via_branch);
 	bencode_get_alt(input, "label", "from-label", &out->label);
@@ -1240,6 +1244,7 @@ void call_ng_free_flags(struct sdp_ng_flags *flags) {
 		g_hash_table_destroy(flags->codec_set);
 	if (flags->sdes_no)
 		g_hash_table_destroy(flags->sdes_no);
+	g_queue_clear_full(&flags->from_tags, free);
 	g_queue_clear_full(&flags->codec_offer, free);
 	g_queue_clear_full(&flags->codec_transcode, free);
 	g_queue_clear_full(&flags->codec_strip, free);
@@ -2115,6 +2120,58 @@ static const char *media_block_match(struct call **call, struct call_monologue *
 
 	return NULL;
 }
+static void add_ml_to_sub_list(GQueue *q, struct call_monologue *ml) {
+	struct call_subscription *cs = g_slice_alloc0(sizeof(*cs));
+	cs->monologue = ml;
+	g_queue_push_tail(q, cs);
+}
+static const char *media_block_match_mult(struct call **call, GQueue *mls,
+		struct sdp_ng_flags *flags, bencode_item_t *input, enum call_opmode opmode)
+{
+	call_ng_process_flags(flags, input, opmode);
+
+	if (!flags->call_id.s)
+		return "No call-id in message";
+	*call = call_get_opmode(&flags->call_id, opmode);
+	if (!*call)
+		return "Unknown call-ID";
+
+	if (flags->all) {
+		// get and add all offer/answer mls
+		for (GList *l = (*call)->monologues.head; l; l = l->next) {
+			struct call_monologue *ml = l->data;
+			if (ml->tagtype != FROM_TAG && ml->tagtype != TO_TAG)
+				continue;
+			add_ml_to_sub_list(mls, ml);
+		}
+		return NULL;
+	}
+
+	// is a single ml given?
+	struct call_monologue *ml = NULL;
+	const char *err = media_block_match1(*call, &ml, flags);
+	if (err)
+		return err;
+	if (ml) {
+		add_ml_to_sub_list(mls, ml);
+		return NULL;
+	}
+
+	// handle from-tag list
+	for (GList *l = flags->from_tags.head; l; l = l->next) {
+		str *s = l->data;
+		struct call_monologue *ml = call_get_monologue(*call, s);
+		if (!ml)
+			ilog(LOG_WARN, "Given from-tag " STR_FORMAT_M " not found", STR_FMT_M(s));
+		else
+			add_ml_to_sub_list(mls, ml);
+	}
+
+	if (!mls->length)
+		return "No monologues matched";
+
+	return NULL;
+}
 
 // XXX these are all identical - unify and use a flags int and/or callback
 const char *call_start_forwarding_ng(bencode_item_t *input, bencode_item_t *output) {
@@ -2585,20 +2642,28 @@ const char *call_subscribe_request_ng(bencode_item_t *input, bencode_item_t *out
 	AUTO_CLEANUP(struct sdp_ng_flags flags, call_ng_free_flags);
 	char rand_buf[65];
 	AUTO_CLEANUP_NULL(struct call *call, call_unlock_release);
-	struct call_monologue *source_ml;
+	AUTO_CLEANUP(GQueue srcs, call_subscriptions_clear) = G_QUEUE_INIT;
+	AUTO_CLEANUP(str sdp_out, str_free_dup) = STR_NULL;
 
 	// get source monologue
-	err = media_block_match(&call, &source_ml, &flags, input, OP_REQUEST);
+	err = media_block_match_mult(&call, &srcs, &flags, input, OP_REQUEST);
 	if (err)
 		return err;
 
 	if (flags.sdp.len)
 		ilog(LOG_INFO, "Subscribe-request with SDP received - ignoring SDP");
 
-	if (!source_ml)
+	if (!srcs.length)
 		return "No call participant specified";
-	if (!source_ml->last_in_sdp.len || !source_ml->last_in_sdp_parsed.length)
-		return "No SDP known for this from-tag";
+
+	// special case with just one source: we can use the original SDP
+	struct call_monologue *sdp_ml = NULL;
+	if (srcs.length == 1) {
+		struct call_subscription *cs = srcs.head->data;
+		sdp_ml = cs->monologue;
+		if (!sdp_ml->last_in_sdp.len || !sdp_ml->last_in_sdp_parsed.length)
+			sdp_ml = NULL;
+	}
 
 	// the `label=` option was possibly used above to select the from-tag --
 	// switch it out with `to-label=` or `set-label=` for monologue_subscribe_request
@@ -2613,20 +2678,52 @@ const char *call_subscribe_request_ng(bencode_item_t *input, bencode_item_t *out
 	}
 	struct call_monologue *dest_ml = call_get_or_create_monologue(call, &flags.to_tag);
 
-	struct sdp_chopper *chopper = sdp_chopper_new(&source_ml->last_in_sdp);
-	bencode_buffer_destroy_add(output->buffer, (free_func_t) sdp_chopper_destroy, chopper);
+	// rewrite SDP, or create new?
+	struct sdp_chopper *chopper = NULL;
+	if (sdp_ml) {
+		chopper = sdp_chopper_new(&sdp_ml->last_in_sdp);
+		bencode_buffer_destroy_add(output->buffer, (free_func_t) sdp_chopper_destroy, chopper);
+	}
 
-	int ret = monologue_subscribe_request(source_ml, dest_ml, &flags);
+	int ret = monologue_subscribe_request(&srcs, dest_ml, &flags);
 	if (ret)
 		return "Failed to request subscription";
 
-	ret = sdp_replace(chopper, &source_ml->last_in_sdp_parsed, dest_ml, &flags);
-	if (ret)
-		return "Failed to rewrite SDP";
+	if (chopper && sdp_ml) {
+		ret = sdp_replace(chopper, &sdp_ml->last_in_sdp_parsed, dest_ml, &flags);
+		if (ret)
+			return "Failed to rewrite SDP";
+	}
+	else {
+		// create new SDP
+		ret = sdp_create(&sdp_out, dest_ml, &flags);
+	}
+
+	// place return output SDP
+	if (chopper) {
+		if (chopper->output->len)
+			bencode_dictionary_add_string_len(output, "sdp", chopper->output->str,
+					chopper->output->len);
+	}
+	else if (sdp_out.len) {
+		bencode_buffer_destroy_add(output->buffer, g_free, sdp_out.s);
+		bencode_dictionary_add_str(output, "sdp", &sdp_out);
+		sdp_out = STR_NULL; // ownership passed to output
+	}
+
+	// add single response ml tag if there's just one, but always add a list
+	if (srcs.length == 1) {
+		struct call_subscription *cs = srcs.head->data;
+		struct call_monologue *source_ml = cs->monologue;
+		bencode_dictionary_add_str_dup(output, "from-tag", &source_ml->tag);
+	}
+	bencode_item_t *from_list = bencode_dictionary_add_list(output, "from-tags");
+	for (GList *l = srcs.head; l; l = l->next) {
+		struct call_subscription *cs = l->data;
+		struct call_monologue *source_ml = cs->monologue;
+		bencode_list_add_str_dup(from_list, &source_ml->tag);
+	}
 
-	if (chopper->output->len)
-		bencode_dictionary_add_string_len(output, "sdp", chopper->output->str, chopper->output->len);
-	bencode_dictionary_add_str_dup(output, "from-tag", &source_ml->tag);
 	bencode_dictionary_add_str_dup(output, "to-tag", &dest_ml->tag);
 
 	return NULL;
diff --git a/daemon/codec.c b/daemon/codec.c
index ca1a9542d..f781411e8 100644
--- a/daemon/codec.c
+++ b/daemon/codec.c
@@ -877,9 +877,9 @@ INLINE struct codec_handler *codec_handler_lookup(GHashTable *ht, int pt, struct
 void codec_handlers_update(struct call_media *receiver, struct call_media *sink,
 		const struct sdp_ng_flags *flags, const struct stream_params *sp)
 {
-	ilogs(codec, LOG_DEBUG, "Setting up codec handlers for " STR_FORMAT_M " -> " STR_FORMAT_M " (media #%u)",
-			STR_FMT_M(&receiver->monologue->tag), STR_FMT_M(&sink->monologue->tag),
-			receiver->index);
+	ilogs(codec, LOG_DEBUG, "Setting up codec handlers for " STR_FORMAT_M " #%u -> " STR_FORMAT_M " #%u",
+			STR_FMT_M(&receiver->monologue->tag), receiver->index,
+			STR_FMT_M(&sink->monologue->tag), sink->index);
 
 	MEDIA_CLEAR(receiver, GENERATOR);
 	MEDIA_CLEAR(sink, GENERATOR);
diff --git a/daemon/janus.c b/daemon/janus.c
index 6a209a8d6..6d90cff1f 100644
--- a/daemon/janus.c
+++ b/daemon/janus.c
@@ -431,7 +431,13 @@ static const char *janus_videoroom_join(struct websocket_message *wm, struct jan
 			flags.no_rtcp_attr = 1;
 			flags.sdes_off = 1;
 
-			int ret = monologue_subscribe_request(source_ml, dest_ml, &flags);
+			AUTO_CLEANUP(GQueue srcs, call_subscriptions_clear) = G_QUEUE_INIT;
+
+			struct call_subscription *cs = g_slice_alloc0(sizeof(*cs));
+			cs->monologue = source_ml;
+			g_queue_push_tail(&srcs, cs);
+
+			int ret = monologue_subscribe_request(&srcs, dest_ml, &flags);
 			if (ret)
 				return "Subscribe error";
 
diff --git a/daemon/redis.c b/daemon/redis.c
index 4fe6902cc..8d60030c7 100644
--- a/daemon/redis.c
+++ b/daemon/redis.c
@@ -1586,7 +1586,7 @@ static int json_link_tags(struct call *c, struct redis_list *tags, struct redis_
 			other_ml = l->data;
 			if (!other_ml)
 			    return -1;
-			__add_subscription(ml, other_ml, true);
+			__add_subscription(ml, other_ml, true, 0);
 		}
 		g_queue_clear(&q);
 
@@ -1596,7 +1596,7 @@ static int json_link_tags(struct call *c, struct redis_list *tags, struct redis_
 			other_ml = l->data;
 			if (!other_ml)
 			    return -1;
-			__add_subscription(ml, other_ml, false);
+			__add_subscription(ml, other_ml, false, 0); // XXX fix indexing
 		}
 		g_queue_clear(&q);
 
@@ -1604,7 +1604,7 @@ static int json_link_tags(struct call *c, struct redis_list *tags, struct redis_
 		if (!ml->subscriptions.length) {
 			other_ml = redis_list_get_ptr(tags, &tags->rh[i], "active");
 			if (other_ml)
-				__add_subscription(ml, other_ml, true);
+				__add_subscription(ml, other_ml, true, 0);
 		}
 
 		if (json_build_list(&q, c, "other_tags", &c->callid, i, tags, root_reader))
diff --git a/include/call.h b/include/call.h
index 35c5592a4..2e26d74ab 100644
--- a/include/call.h
+++ b/include/call.h
@@ -403,6 +403,7 @@ struct call_media {
 struct call_subscription {
 	struct call_monologue	*monologue;
 	GList			*link; // link into the corresponding opposite list
+	unsigned int		media_offset; // 0 if media indexes match up
 	unsigned int		offer_answer:1; // bidirectional, exclusive
 };
 
@@ -600,11 +601,13 @@ struct call_monologue *__monologue_create(struct call *call);
 void __monologue_tag(struct call_monologue *ml, const str *tag);
 void __monologue_viabranch(struct call_monologue *ml, const str *viabranch);
 struct packet_stream *__packet_stream_new(struct call *call);
-void __add_subscription(struct call_monologue *ml, struct call_monologue *other, bool offer_answer);
+void __add_subscription(struct call_monologue *ml, struct call_monologue *other, bool offer_answer,
+		unsigned int media_offset);
 void free_sink_handler(void *);
 void __add_sink_handler(GQueue *, struct packet_stream *);
 
 void call_subscription_free(void *);
+void call_subscriptions_clear(GQueue *q);
 
 
 struct call *call_get_or_create(const str *callid, bool foreign, bool exclusive);
@@ -620,7 +623,7 @@ int monologue_offer_answer(struct call_monologue *dialogue[2], GQueue *streams,
 void codecs_offer_answer(struct call_media *media, struct call_media *other_media,
 		struct stream_params *sp, struct sdp_ng_flags *flags);
 int monologue_publish(struct call_monologue *ml, GQueue *streams, struct sdp_ng_flags *flags);
-int monologue_subscribe_request(struct call_monologue *src, struct call_monologue *dst, struct sdp_ng_flags *);
+int monologue_subscribe_request(const GQueue *srcs, struct call_monologue *dst, struct sdp_ng_flags *);
 int monologue_subscribe_answer(struct call_monologue *dst, struct sdp_ng_flags *,
 		GQueue *);
 int monologue_unsubscribe(struct call_monologue *dst, struct sdp_ng_flags *);
diff --git a/include/call_interfaces.h b/include/call_interfaces.h
index 1cb4defef..01584a074 100644
--- a/include/call_interfaces.h
+++ b/include/call_interfaces.h
@@ -21,6 +21,7 @@ struct sdp_ng_flags {
 	enum call_opmode opmode;
 	str call_id;
 	str from_tag;
+	GQueue from_tags;
 	str to_tag;
 	str via_branch;
 	str sdp;
diff --git a/perl/NGCP/Rtpengine/AutoTest.pm b/perl/NGCP/Rtpengine/AutoTest.pm
index d28002eba..e4cd6790e 100644
--- a/perl/NGCP/Rtpengine/AutoTest.pm
+++ b/perl/NGCP/Rtpengine/AutoTest.pm
@@ -160,7 +160,7 @@ sub subscribe_request {
 	my ($name, $req, $sdp_exp) = @_;
 	my $resp = rtpe_req('subscribe request', $name, $req);
 	my @matches = sdp_match('subscribe request', $name, $resp->{sdp}, $sdp_exp);
-	return ($resp->{'from-tag'}, $resp->{'to-tag'}, @matches);
+	return ($resp->{'from-tag'}, $resp->{'to-tag'}, $resp->{'from-tags'}, @matches);
 }
 sub subscribe_answer {
 	my ($name, $req, $sdp) = @_;
diff --git a/t/auto-daemon-tests-pubsub.pl b/t/auto-daemon-tests-pubsub.pl
index 28da91a0f..87dffae30 100755
--- a/t/auto-daemon-tests-pubsub.pl
+++ b/t/auto-daemon-tests-pubsub.pl
@@ -11,16 +11,419 @@ use POSIX;
 
 
 autotest_start(qw(--config-file=none -t -1 -i 203.0.113.1 -i 2001:db8:4321::1
-			-n 2223 -c 12345 -f -L 7 -E -u 2222 --silence-detect=1))
+			-n 2223 -c 12345 -f -L 7 -E -u 2222 --log-level-internals=7))
 		or die;
 
 
 
 my ($sock_a, $sock_b, $sock_c, $sock_d, $port_a, $port_b, $port_c, $ssrc_a, $ssrc_b, $resp,
-	$sock_ax, $sock_bx, $port_ax, $port_bx,
+	$sock_ax, $sock_bx, $port_ax, $port_bx, $port_d,
 	$srtp_ctx_a, $srtp_ctx_b, $srtp_ctx_a_rev, $srtp_ctx_b_rev, $ufrag_a, $ufrag_b,
 	@ret1, @ret2, @ret3, @ret4, $srtp_key_a, $srtp_key_b, $ts, $seq,
-	$ftr, $ttr, $ttr2);
+	$ftr, $ttr, $fts, $ttr2);
+
+
+
+
+
+($sock_a, $sock_b, $sock_c, $sock_d) =
+	new_call([qw(198.51.100.14 6080)], [qw(198.51.100.14 6082)], [qw(198.51.100.14 6084)],
+			[qw(198.51.100.14 6086)]);
+
+($port_a) = offer('"all" sub',
+	{ }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6080 RTP/AVP 0 8
+c=IN IP4 198.51.100.14
+a=sendrecv
+----------------------------------
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendrecv
+a=rtcp:PORT
+SDP
+
+($port_b) = answer('"all" sub',
+	{ }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6082 RTP/AVP 0 8
+c=IN IP4 198.51.100.14
+a=sendrecv
+----------------------------------
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendrecv
+a=rtcp:PORT
+SDP
+
+
+snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
+rcv($sock_a, $port_b, rtpm(0, 2000, 4000, 0x3456, "\x00" x 160));
+snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
+rcv($sock_b, $port_a, rtpm(0, 4000, 7000, 0x6543, "\x00" x 160));
+
+($ftr, $ttr, $fts, $port_c, undef, $port_d) = subscribe_request('"all" sub',
+	{ 'flags' => ['all'] }, <<SDP);
+v=0
+o=- SDP_VERSION IN IP4 203.0.113.1
+s=RTPE_VERSION
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendonly
+a=rtcp:PORT
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendonly
+a=rtcp:PORT
+SDP
+
+is $ftr, undef, 'from-tag matches';
+is_deeply $fts, [ft(), tt()], 'from-tags match';
+
+subscribe_answer('"all" sub',
+	{ 'to-tag' => $ttr, flags => ['allow transcoding'] }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6084 RTP/AVP 8
+c=IN IP4 198.51.100.14
+a=recvonly
+m=audio 6086 RTP/AVP 8
+c=IN IP4 198.51.100.14
+a=recvonly
+SDP
+
+snd($sock_a, $port_b, rtp(0, 4001, 7160, 0x6543, "\x00" x 160));
+rcv($sock_b, $port_a, rtpm(0, 4001, 7160, 0x6543, "\x00" x 160));
+rcv($sock_c, $port_c, rtpm(8, 4001, 7160, -1, "\x2a" x 160));
+snd($sock_b, $port_a, rtp(0, 2001, 4160, 0x3456, "\x00" x 160));
+rcv($sock_a, $port_b, rtpm(0, 2001, 4160, 0x3456, "\x00" x 160));
+rcv($sock_d, $port_d, rtpm(8, 2001, 4160, -1, "\x2a" x 160));
+
+
+
+
+($sock_a, $sock_b, $sock_c, $sock_d) =
+	new_call([qw(198.51.100.14 6088)], [qw(198.51.100.14 6090)], [qw(198.51.100.14 6092)],
+			[qw(198.51.100.14 6094)]);
+
+($port_a) = offer('sub to multiple tags',
+	{ }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6088 RTP/AVP 0 8
+c=IN IP4 198.51.100.14
+a=sendrecv
+----------------------------------
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendrecv
+a=rtcp:PORT
+SDP
+
+($port_b) = answer('sub to multiple tags',
+	{ }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6090 RTP/AVP 0 8
+c=IN IP4 198.51.100.14
+a=sendrecv
+----------------------------------
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendrecv
+a=rtcp:PORT
+SDP
+
+
+snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
+rcv($sock_a, $port_b, rtpm(0, 2000, 4000, 0x3456, "\x00" x 160));
+snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
+rcv($sock_b, $port_a, rtpm(0, 4000, 7000, 0x6543, "\x00" x 160));
+
+($ftr, $ttr, $fts, $port_c, undef, $port_d) = subscribe_request('sub to multiple tags',
+	{ 'from-tags' => [ft(), tt()] }, <<SDP);
+v=0
+o=- SDP_VERSION IN IP4 203.0.113.1
+s=RTPE_VERSION
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendonly
+a=rtcp:PORT
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendonly
+a=rtcp:PORT
+SDP
+
+is $ftr, undef, 'from-tag matches';
+is_deeply $fts, [ft(), tt()], 'from-tags match';
+
+subscribe_answer('sub to multiple tags',
+	{ 'to-tag' => $ttr, flags => ['allow transcoding'] }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6092 RTP/AVP 8
+c=IN IP4 198.51.100.14
+a=recvonly
+m=audio 6094 RTP/AVP 8
+c=IN IP4 198.51.100.14
+a=recvonly
+SDP
+
+snd($sock_a, $port_b, rtp(0, 4001, 7160, 0x6543, "\x00" x 160));
+rcv($sock_b, $port_a, rtpm(0, 4001, 7160, 0x6543, "\x00" x 160));
+rcv($sock_c, $port_c, rtpm(8, 4001, 7160, -1, "\x2a" x 160));
+snd($sock_b, $port_a, rtp(0, 2001, 4160, 0x3456, "\x00" x 160));
+rcv($sock_a, $port_b, rtpm(0, 2001, 4160, 0x3456, "\x00" x 160));
+rcv($sock_d, $port_d, rtpm(8, 2001, 4160, -1, "\x2a" x 160));
+
+
+
+($sock_a, $sock_b, $sock_c, $sock_d) =
+	new_call([qw(198.51.100.14 6096)], [qw(198.51.100.14 6098)], [qw(198.51.100.14 6100)],
+			[qw(198.51.100.14 6102)]);
+
+($port_a) = offer('sub to multiple tags via flags',
+	{ }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6096 RTP/AVP 0 8
+c=IN IP4 198.51.100.14
+a=sendrecv
+----------------------------------
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendrecv
+a=rtcp:PORT
+SDP
+
+($port_b) = answer('sub to multiple tags via flags',
+	{ }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6098 RTP/AVP 0 8
+c=IN IP4 198.51.100.14
+a=sendrecv
+----------------------------------
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendrecv
+a=rtcp:PORT
+SDP
+
+
+snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
+rcv($sock_a, $port_b, rtpm(0, 2000, 4000, 0x3456, "\x00" x 160));
+snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
+rcv($sock_b, $port_a, rtpm(0, 4000, 7000, 0x6543, "\x00" x 160));
+
+($ftr, $ttr, $fts, $port_c, undef, $port_d) = subscribe_request('sub to multiple tags via flags',
+	{ flags => ['from-tags-' . ft(), 'from-tags-' . tt()] }, <<SDP);
+v=0
+o=- SDP_VERSION IN IP4 203.0.113.1
+s=RTPE_VERSION
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendonly
+a=rtcp:PORT
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendonly
+a=rtcp:PORT
+SDP
+
+is $ftr, undef, 'from-tag matches';
+is_deeply $fts, [ft(), tt()], 'from-tags match';
+
+subscribe_answer('sub to multiple tags via flags',
+	{ 'to-tag' => $ttr, flags => ['allow transcoding'] }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6100 RTP/AVP 8
+c=IN IP4 198.51.100.14
+a=recvonly
+m=audio 6102 RTP/AVP 8
+c=IN IP4 198.51.100.14
+a=recvonly
+SDP
+
+snd($sock_a, $port_b, rtp(0, 4001, 7160, 0x6543, "\x00" x 160));
+rcv($sock_b, $port_a, rtpm(0, 4001, 7160, 0x6543, "\x00" x 160));
+rcv($sock_c, $port_c, rtpm(8, 4001, 7160, -1, "\x2a" x 160));
+snd($sock_b, $port_a, rtp(0, 2001, 4160, 0x3456, "\x00" x 160));
+rcv($sock_a, $port_b, rtpm(0, 2001, 4160, 0x3456, "\x00" x 160));
+rcv($sock_d, $port_d, rtpm(8, 2001, 4160, -1, "\x2a" x 160));
+
+
+
+($sock_a, $sock_b, $sock_c, $sock_d) =
+	new_call([qw(198.51.100.14 6104)], [qw(198.51.100.14 6106)], [qw(198.51.100.14 6108)],
+			[qw(198.51.100.14 6110)]);
+
+($port_a) = offer('sub to multiple tags - reverse',
+	{ }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6104 RTP/AVP 0 8
+c=IN IP4 198.51.100.14
+a=sendrecv
+----------------------------------
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendrecv
+a=rtcp:PORT
+SDP
+
+($port_b) = answer('sub to multiple tags - reverse',
+	{ }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6106 RTP/AVP 0 8
+c=IN IP4 198.51.100.14
+a=sendrecv
+----------------------------------
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendrecv
+a=rtcp:PORT
+SDP
+
+
+snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
+rcv($sock_a, $port_b, rtpm(0, 2000, 4000, 0x3456, "\x00" x 160));
+snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
+rcv($sock_b, $port_a, rtpm(0, 4000, 7000, 0x6543, "\x00" x 160));
+
+($ftr, $ttr, $fts, $port_c, undef, $port_d) = subscribe_request('sub to multiple tags - reverse',
+	{ 'from-tags' => [tt(), ft()] }, <<SDP);
+v=0
+o=- SDP_VERSION IN IP4 203.0.113.1
+s=RTPE_VERSION
+t=0 0
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendonly
+a=rtcp:PORT
+m=audio PORT RTP/AVP 0 8
+c=IN IP4 203.0.113.1
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=sendonly
+a=rtcp:PORT
+SDP
+
+is $ftr, undef, 'from-tag matches';
+is_deeply $fts, [tt(), ft()], 'from-tags match';
+
+subscribe_answer('sub to multiple tags - reverse',
+	{ 'to-tag' => $ttr, flags => ['allow transcoding'] }, <<SDP);
+v=0
+o=- 1545997027 1 IN IP4 198.51.100.1
+s=tester
+t=0 0
+m=audio 6108 RTP/AVP 8
+c=IN IP4 198.51.100.14
+a=recvonly
+m=audio 6110 RTP/AVP 8
+c=IN IP4 198.51.100.14
+a=recvonly
+SDP
+
+snd($sock_a, $port_b, rtp(0, 4001, 7160, 0x6543, "\x00" x 160));
+rcv($sock_b, $port_a, rtpm(0, 4001, 7160, 0x6543, "\x00" x 160));
+rcv($sock_d, $port_d, rtpm(8, 4001, 7160, -1, "\x2a" x 160));
+snd($sock_b, $port_a, rtp(0, 2001, 4160, 0x3456, "\x00" x 160));
+rcv($sock_a, $port_b, rtpm(0, 2001, 4160, 0x3456, "\x00" x 160));
+rcv($sock_c, $port_c, rtpm(8, 2001, 4160, -1, "\x2a" x 160));
 
 
 
@@ -79,7 +482,7 @@ snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
 snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
 ($ssrc_b) = rcv($sock_b, $port_a, rtpm(0, 4000, 7000, -1, "\x00" x 160));
 
-($ftr, $ttr, $port_c) = subscribe_request('sub, multi codec, sub w diff codec',
+($ftr, $ttr, undef, $port_c) = subscribe_request('sub, multi codec, sub w diff codec',
 	{ 'from-tag' => ft() }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -167,7 +570,7 @@ snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
 snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
 ($ssrc_b) = rcv($sock_b, $port_a, rtpm(0, 4000, 7000, -1, "\x00" x 160));
 
-($ftr, $ttr, $port_c) = subscribe_request('sub w tc - acc',
+($ftr, $ttr, undef, $port_c) = subscribe_request('sub w tc - acc',
 	{ 'from-tag' => ft(), codec => { transcode => ['PCMA'] } }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -254,7 +657,7 @@ snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
 snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
 ($ssrc_b) = rcv($sock_b, $port_a, rtpm(0, 4000, 7000, -1, "\x00" x 160));
 
-($ftr, $ttr, $port_c) = subscribe_request('sub w tc - rej',
+($ftr, $ttr, undef, $port_c) = subscribe_request('sub w tc - rej',
 	{ 'from-tag' => ft(), codec => { transcode => ['PCMA'] } }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -341,7 +744,7 @@ snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
 snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
 ($ssrc_b) = rcv($sock_b, $port_a, rtpm(0, 4000, 7000, -1, "\x00" x 160));
 
-($ftr, $ttr, $port_c) = subscribe_request('simple sub',
+($ftr, $ttr, undef, $port_c) = subscribe_request('simple sub',
 	{ 'from-tag' => ft() }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -427,7 +830,7 @@ snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
 snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
 ($ssrc_b) = rcv($sock_b, $port_a, rtpm(0, 4000, 7000, -1, "\x00" x 160));
 
-($ftr, $ttr, $port_c) = subscribe_request('simple sub w label',
+($ftr, $ttr, undef, $port_c) = subscribe_request('simple sub w label',
 	{ label => 'foo' }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -513,7 +916,7 @@ snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
 snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
 ($ssrc_b) = rcv($sock_b, $port_a, rtpm(0, 4000, 7000, -1, "\x00" x 160));
 
-($ftr, $ttr, $port_c) = subscribe_request('simple sub w to-tag label',
+($ftr, $ttr, undef, $port_c) = subscribe_request('simple sub w to-tag label',
 	{ label => 'bar' }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -599,7 +1002,7 @@ snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
 snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
 ($ssrc_b) = rcv($sock_b, $port_a, rtpm(0, 4000, 7000, -1, "\x00" x 160));
 
-($ftr, $ttr, $port_c, undef, $srtp_key_a) = subscribe_request('SRTP sub',
+($ftr, $ttr, undef, $port_c, undef, $srtp_key_a) = subscribe_request('SRTP sub',
 	{ 'from-tag' => ft(), 'transport-protocol' => 'RTP/SAVP',
 	SDES => ['no-AEAD_AES_256_GCM', 'no-AEAD_AES_128_GCM'] }, <<SDP);
 v=0
@@ -705,7 +1108,7 @@ snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
 snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
 ($ssrc_b) = rcv($sock_b, $port_a, rtpm(0, 4000, 7000, -1, "\x00" x 160));
 
-($ftr, $ttr, $port_c, undef, undef, undef, undef, undef, $srtp_key_a) = subscribe_request('SRTP sub',
+($ftr, $ttr, undef, $port_c, undef, undef, undef, undef, undef, $srtp_key_a) = subscribe_request('SRTP sub',
 	{ 'from-tag' => ft(), 'transport-protocol' => 'RTP/SAVP',
 	SDES => ['no-AEAD_AES_256_GCM', 'no-AEAD_AES_128_GCM'] }, <<SDP);
 v=0
@@ -838,7 +1241,7 @@ srtp_snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160), $srtp_ctx_b
 srtp_snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160), $srtp_ctx_a);
 ($ssrc_b) = srtp_rcv($sock_b, $port_a, rtpm(0, 4000, 7000, -1, "\x00" x 160), $srtp_ctx_a);
 
-($ftr, $ttr, $port_c) = subscribe_request('SRTP call RTP sub',
+($ftr, $ttr, undef, $port_c) = subscribe_request('SRTP call RTP sub',
 	{ 'from-tag' => ft(), 'transport-protocol' => 'RTP/AVP', }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -925,7 +1328,7 @@ snd($sock_b, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
 snd($sock_a, $port_b, rtp(0, 4000, 7000, 0x6543, "\x00" x 160));
 ($ssrc_b) = rcv($sock_b, $port_a, rtpm(0, 4000, 7000, -1, "\x00" x 160));
 
-($ftr, $ttr, $port_c) = subscribe_request('ICE sub',
+($ftr, $ttr, undef, $port_c) = subscribe_request('ICE sub',
 	{ 'from-tag' => ft(), ICE => 'force' }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -997,7 +1400,7 @@ SDP
 
 snd($sock_a, $port_a, rtp(0, 2000, 4000, 0x3456, "\x00" x 160));
 
-($ftr, $ttr, $port_b) = subscribe_request('publish/subscribe',
+($ftr, $ttr, undef, $port_b) = subscribe_request('publish/subscribe',
 	{ 'from-tag' => ft() }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -1026,7 +1429,7 @@ SDP
 snd($sock_a, $port_a, rtp(0, 2001, 4160, 0x3456, "\x00" x 160));
 rcv($sock_b, $port_b, rtpm(0, 2001, 4160, 0x3456, "\x00" x 160));
 
-($ftr, $ttr, $port_b) = subscribe_request('publish/subscribe',
+($ftr, $ttr, undef, $port_b) = subscribe_request('publish/subscribe',
 	{ 'from-tag' => ft() }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -1087,7 +1490,7 @@ SDP
 
 snd($sock_a, $port_a, rtp(8, 2000, 4000, 0x3456, "\x00" x 160));
 
-($ftr, $ttr, $port_b) = subscribe_request('publish/subscribe w codec-accept',
+($ftr, $ttr, undef, $port_b) = subscribe_request('publish/subscribe w codec-accept',
 	{ 'from-tag' => ft() }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -1116,7 +1519,7 @@ SDP
 snd($sock_a, $port_a, rtp(8, 2001, 4160, 0x3456, "\x00" x 160));
 rcv($sock_b, $port_b, rtpm(8, 2001, 4160, 0x3456, "\x00" x 160));
 
-($ftr, $ttr, $port_b) = subscribe_request('publish/subscribe w codec-accept',
+($ftr, $ttr, undef, $port_b) = subscribe_request('publish/subscribe w codec-accept',
 	{ 'from-tag' => ft() }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -1177,7 +1580,7 @@ SDP
 
 snd($sock_a, $port_a, rtp(8, 2000, 4000, 0x3456, "\x00" x 160));
 
-($ftr, $ttr, $port_b) = subscribe_request('publish/subscribe w unsupp and t/c',
+($ftr, $ttr, undef, $port_b) = subscribe_request('publish/subscribe w unsupp and t/c',
 	{ 'from-tag' => ft() }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
@@ -1206,7 +1609,7 @@ SDP
 snd($sock_a, $port_a, rtp(8, 2001, 4160, 0x3456, "\x00" x 160));
 rcv($sock_b, $port_b, rtpm(8, 2001, 4160, 0x3456, "\x00" x 160));
 
-($ftr, $ttr, $port_b) = subscribe_request('publish/subscribe w unsupp and t/c',
+($ftr, $ttr, undef, $port_b) = subscribe_request('publish/subscribe w unsupp and t/c',
 	{ 'from-tag' => ft(), codec => { strip => ['PCMA'], transcode => ['PCMU'] } }, <<SDP);
 v=0
 o=- 1545997027 1 IN IP4 198.51.100.1
diff --git a/utils/rtpengine-ng-client b/utils/rtpengine-ng-client
index 8bedb6a18..84cf1753e 100755
--- a/utils/rtpengine-ng-client
+++ b/utils/rtpengine-ng-client
@@ -99,6 +99,7 @@ GetOptions(
 	'set-label=s'			=> \$options{'set-label'},
 	'from-label=s'			=> \$options{'from-label'},
 	'to-label=s'			=> \$options{'to-label'},
+	'from-tags=s@'			=> \$options{'from-tags'},
 ) or die;
 
 my $cmd = shift(@ARGV) or die;
@@ -117,7 +118,7 @@ for my $x (split(/,/, 'trust address,symmetric,asymmetric,unidirectional,force,s
 for my $x (split(/,/, 'origin,session connection,sdp version,username,session-name,zero-address')) {
 	defined($options{'replace-' . $x}) and push(@{$packet{replace}}, $x);
 }
-for my $x (split(/,/, 'rtcp-mux,SDES,supports,T.38,OSRTP,received-from')) {
+for my $x (split(/,/, 'rtcp-mux,SDES,supports,T.38,OSRTP,received-from,from-tags')) {
 	$packet{$x} = $options{$x}
 		if defined($options{$x}) && ref($options{$x}) eq 'ARRAY';
 }