diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..dc04f7720588c21499229f608acd815fe6c71aca
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,33 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+# Preamble
+
+This changelog file was started on October 2019. Previous changes were more or less tracked in the *NEWS* file.
+
+## [Unreleased]
+    
+### Added
+- RTP bundle mode feature according to https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-54 .
+
+### Changed
+- Big internal refactoring of how streams are managed within offer/answer exchanges.
+- ICE now uses all IP addresses detected on the host.
+- Better handling of parameter changes in streams during the session, which avoids unecessary restarts.
+
+### Fixed
+- Internal refactoring of management of locally played tones, in order to fix race conditions.
+
+
+## [4.3.0] - 2019-10-14
+
+### Added
+- New cmake options to make "small" builds of liblinphone, by excluding adavanced IM and DB storage.
+
+### Changed
+- Optimisations in chatrooms loading from Sqlite DB, improving startup time.
+- License changed to GNU GPLv3.
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8053fadbaa489e11c2dd025c231a2c627bc772fc..6ad2adde083361593b2fec4c4cfac58f628662e7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -97,6 +97,8 @@ include(CMakePushCheckState)
 include(GNUInstallDirs)
 include(CheckCXXCompilerFlag)
 
+check_symbol_exists(getifaddrs "sys/types.h;ifaddrs.h" HAVE_GETIFADDRS)
+
 if(NOT CMAKE_INSTALL_RPATH AND CMAKE_INSTALL_PREFIX)
 	set(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR})
 	message(STATUS "Setting install rpath to ${CMAKE_INSTALL_RPATH}")
@@ -284,10 +286,8 @@ if(MSVC)
 else()
 	list(APPEND STRICT_OPTIONS_CPP
 		"-Wall"
-		"-Wcast-align"
 		"-Wconversion"
 		"-Werror=return-type"
-		"-Wfloat-equal"
 		"-Winit-self"
 		"-Wno-error=deprecated-declarations"
 		"-Wpointer-arith"
diff --git a/ChangeLog.md b/ChangeLog.md
deleted file mode 100644
index aaec8bb0f71d9203b052a3fd68e840a8e222efbf..0000000000000000000000000000000000000000
--- a/ChangeLog.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Change Log
-All notable changes to this project will be documented in this file.
-
-Group changes to describe their impact on the project, as follows:
-
-    Added for new features.
-    Changed for changes in existing functionality.
-    Deprecated for once-stable features removed in upcoming releases.
-    Removed for deprecated features removed in this release.
-    Fixed for any bug fixes.
-    Security to invite users to upgrade in case of vulnerabilities.
-    
-# Preamble
-
-This changelog file was started on October 2019. Previous changes were more or less tracked in the *NEWS* file.
-
-## [4.3.0] - 2019-10-14
-
-### Added
-- New cmake options to make "small" builds of liblinphone, by excluding adavanced IM and DB storage.
-
-### Changed
-- Optimisations in chatrooms loading from Sqlite DB, improving startup time.
-- License changed to GNU GPLv3.
-
diff --git a/build/rpm/liblinphone.spec.cmake b/build/rpm/liblinphone.spec.cmake
index 03fcc579ea757779a6a8dbf11cd7035e00ad6174..e6b799a40ff54c88bfe7447901964609e43e2a2b 100755
--- a/build/rpm/liblinphone.spec.cmake
+++ b/build/rpm/liblinphone.spec.cmake
@@ -89,7 +89,7 @@ rm -rf $RPM_BUILD_ROOT
 
 %files
 %defattr(-,root,root)
-%doc ChangeLog.md LICENSE.txt README.md
+%doc CHANGELOG.md LICENSE.txt README.md
 %if @ENABLE_DAEMON@ || @ENABLE_CONSOLE_UI@ || @ENABLE_TOOLS@
 %{_bindir}/*
 %endif
diff --git a/config.h.cmake b/config.h.cmake
index 31a339a26be3b34f69b09fcc00d4ff03fc84f0fb..ab8b6b0b07efe03c6a5a9451f2871427b5d91f05 100644
--- a/config.h.cmake
+++ b/config.h.cmake
@@ -50,3 +50,5 @@
 #cmakedefine HAVE_ADVANCED_IM
 #cmakedefine HAVE_DB_STORAGE
 #cmakedefine ENABLE_UPDATE_CHECK 1
+#cmakedefine HAVE_GETIFADDRS
+
diff --git a/coreapi/bellesip_sal/sal_sdp.c b/coreapi/bellesip_sal/sal_sdp.c
index 2892c77f71fd03f0f3f74fe00ae1580c30ef8766..c87361e8d5f985ea92b64c7666fed999447d0399 100644
--- a/coreapi/bellesip_sal/sal_sdp.c
+++ b/coreapi/bellesip_sal/sal_sdp.c
@@ -192,6 +192,20 @@ static belle_sdp_attribute_t * create_rtcp_xr_attribute(const OrtpRtcpXrConfigur
 	return BELLE_SDP_ATTRIBUTE(attribute);
 }
 
+static void add_mid_attributes(belle_sdp_media_description_t *media_desc, const SalStreamDescription *stream){
+	if (stream->mid[0] != '\0'){
+		belle_sdp_media_description_add_attribute(media_desc, belle_sdp_attribute_create("mid", stream->mid));
+	}
+	if (stream->mid_rtp_ext_header_id){
+		char *value = bctbx_strdup_printf("%i urn:ietf:params:rtp-hdrext:sdes:mid", stream->mid_rtp_ext_header_id); 
+		belle_sdp_media_description_add_attribute(media_desc, belle_sdp_attribute_create("extmap", value));
+		bctbx_free(value);
+	}
+	if (stream->bundle_only){
+		belle_sdp_media_description_add_attribute(media_desc, belle_sdp_attribute_create("bundle-only", NULL));
+	}
+}
+
 static void stream_description_to_sdp ( belle_sdp_session_description_t *session_desc, const SalMediaDescription *md, const SalStreamDescription *stream ) {
 	belle_sdp_mime_parameter_t* mime_param;
 	belle_sdp_media_description_t* media_desc;
@@ -205,6 +219,7 @@ static void stream_description_to_sdp ( belle_sdp_session_description_t *session
 	int rtp_port;
 	int rtcp_port;
 	bool_t different_rtp_and_rtcp_addr;
+	bool_t stream_enabled = sal_stream_description_enabled(stream);
 
 	rtp_addr=stream->rtp_addr;
 	rtcp_addr=stream->rtcp_addr;
@@ -318,7 +333,8 @@ static void stream_description_to_sdp ( belle_sdp_session_description_t *session
 	if (stream->rtcp_mux){
 		belle_sdp_media_description_add_attribute(media_desc, belle_sdp_attribute_create ("rtcp-mux",NULL ) );
 	}
-
+	add_mid_attributes(media_desc, stream);
+	
 	if (rtp_port != 0) {
 		different_rtp_and_rtcp_addr = (rtcp_addr[0] != '\0') && (strcmp(rtp_addr, rtcp_addr) != 0);
 		if ((rtcp_port != (rtp_port + 1)) || (different_rtp_and_rtcp_addr == TRUE)) {
@@ -346,11 +362,11 @@ static void stream_description_to_sdp ( belle_sdp_session_description_t *session
 		}
 	}
 
-	if ((rtp_port != 0) && (sal_stream_description_has_avpf(stream) || sal_stream_description_has_implicit_avpf(stream))) {
+	if (stream_enabled && (sal_stream_description_has_avpf(stream) || sal_stream_description_has_implicit_avpf(stream))) {
 		add_rtcp_fb_attributes(media_desc, md, stream);
 	}
 
-	if ((rtp_port != 0) && (stream->rtcp_xr.enabled == TRUE)) {
+	if (stream_enabled && (stream->rtcp_xr.enabled == TRUE)) {
 		char sastr[1024] = {0};
 		char mastr[1024] = {0};
 		size_t saoff = 0;
@@ -391,7 +407,25 @@ static void stream_description_to_sdp ( belle_sdp_session_description_t *session
 	belle_sdp_session_description_add_media_description(session_desc, media_desc);
 }
 
-belle_sdp_session_description_t * media_description_to_sdp ( const SalMediaDescription *desc ) {
+static void bundles_to_sdp(const bctbx_list_t *bundles, belle_sdp_session_description_t *session_desc){
+	const bctbx_list_t *elem;
+	for (elem = bundles; elem != NULL; elem = elem->next){
+		SalStreamBundle *bundle = (SalStreamBundle*) elem->data;
+		const bctbx_list_t * id_iterator;
+		char *attr_value = ms_strdup("BUNDLE");
+		for (id_iterator = bundle->mids; id_iterator != NULL; id_iterator = id_iterator->next){
+			const char *mid = (const char*) id_iterator->data;
+			char *tmp = ms_strdup_printf("%s %s", attr_value, mid);
+			ms_free(attr_value);
+			attr_value = tmp;
+			
+		}
+		belle_sdp_session_description_add_attribute(session_desc, belle_sdp_attribute_create("group", attr_value));
+		bctbx_free(attr_value);
+	}
+}
+
+belle_sdp_session_description_t * media_description_to_sdp(const SalMediaDescription *desc) {
 	belle_sdp_session_description_t* session_desc=belle_sdp_session_description_new();
 	bool_t inet6;
 	belle_sdp_origin_t* origin;
@@ -440,6 +474,10 @@ belle_sdp_session_description_t * media_description_to_sdp ( const SalMediaDescr
 	if (desc->rtcp_xr.enabled == TRUE) {
 		belle_sdp_session_description_add_attribute(session_desc, create_rtcp_xr_attribute(&desc->rtcp_xr));
 	}
+	
+	if (desc->bundles)
+		bundles_to_sdp(desc->bundles, session_desc);
+	
 	if (desc->custom_sdp_attributes) {
 		belle_sdp_session_description_t *custom_desc = (belle_sdp_session_description_t *)desc->custom_sdp_attributes;
 		belle_sip_list_t *l = belle_sdp_session_description_get_attributes(custom_desc);
@@ -807,6 +845,14 @@ static SalStreamDescription * sdp_to_stream_description(SalMediaDescription *md,
 	}
 
 	stream->rtcp_mux = belle_sdp_media_description_get_attribute(media_desc, "rtcp-mux") != NULL;
+	stream->bundle_only = belle_sdp_media_description_get_attribute(media_desc, "bundle-only") != NULL;
+	
+	attribute = belle_sdp_media_description_get_attribute(media_desc, "mid");
+	if (attribute){
+		value = belle_sdp_attribute_get_value(attribute);
+		if (value)
+			strncpy(stream->mid, value, sizeof(stream->mid) - 1);
+	}
 
 	/* Get media payload types */
 	sdp_parse_payload_types(media_desc, stream);
@@ -876,18 +922,45 @@ static SalStreamDescription * sdp_to_stream_description(SalMediaDescription *md,
 	stream->rtcp_xr = md->rtcp_xr;	// Use session parameters if no stream parameters are defined
 	sdp_parse_media_rtcp_xr_parameters(media_desc, &stream->rtcp_xr);
 
-	/* Get the custom attributes */
+	/* Get the custom attributes, and parse some 'extmap'*/
 	for (custom_attribute_it = belle_sdp_media_description_get_attributes(media_desc); custom_attribute_it != NULL; custom_attribute_it = custom_attribute_it->next) {
 		belle_sdp_attribute_t *attr = (belle_sdp_attribute_t *)custom_attribute_it->data;
-		stream->custom_sdp_attributes = sal_custom_sdp_attribute_append(stream->custom_sdp_attributes, belle_sdp_attribute_get_name(attr), belle_sdp_attribute_get_value(attr));
+		const char *attr_name = belle_sdp_attribute_get_name(attr);
+		const char *attr_value = belle_sdp_attribute_get_value(attr);
+		stream->custom_sdp_attributes = sal_custom_sdp_attribute_append(stream->custom_sdp_attributes, attr_name, attr_value);
+		
+		if (strcasecmp(attr_name, "extmap") == 0){
+			char *extmap_urn = (char*)bctbx_malloc0(strlen(attr_value) + 1);
+			int rtp_ext_header_id = 0;
+			if (sscanf(attr_value, "%i %s", &rtp_ext_header_id, extmap_urn) > 0
+				&& strcasecmp(extmap_urn, "urn:ietf:params:rtp-hdrext:sdes:mid") == 0){
+				stream->mid_rtp_ext_header_id = rtp_ext_header_id;
+			}
+			bctbx_free(extmap_urn);
+		}
 	}
 
 	md->nb_streams++;
 	return stream;
 }
 
+static void add_bundles(SalMediaDescription *desc, const char *ids){
+	char *tmp = (char*)ms_malloc0(strlen(ids) + 1);
+	int err;
+	SalStreamBundle *bundle = sal_media_description_add_new_bundle(desc);
+	do{
+		int consumed = 0;
+		err = sscanf(ids, "%s%n", tmp, &consumed);
+		if (err > 0){
+			bundle->mids = bctbx_list_append(bundle->mids, bctbx_strdup(tmp));
+			ids += consumed;
+		}else break;
+	}while( *ids != '\0');
+	ms_free(tmp);
+}
+
 
-int sdp_to_media_description ( belle_sdp_session_description_t  *session_desc, SalMediaDescription *desc ) {
+int sdp_to_media_description( belle_sdp_session_description_t  *session_desc, SalMediaDescription *desc ) {
 	belle_sdp_connection_t* cnx;
 	belle_sip_list_t* media_desc_it;
 	belle_sdp_media_description_t* media_desc;
@@ -954,10 +1027,17 @@ int sdp_to_media_description ( belle_sdp_session_description_t  *session_desc, S
 	/* Get session RTCP-XR attributes if any */
 	sdp_parse_session_rtcp_xr_parameters(session_desc, &desc->rtcp_xr);
 
-	/* Get the custom attributes */
+	/* Get the custom attributes, parse some of them that are relevant */
 	for (custom_attribute_it = belle_sdp_session_description_get_attributes(session_desc); custom_attribute_it != NULL; custom_attribute_it = custom_attribute_it->next) {
 		belle_sdp_attribute_t *attr = (belle_sdp_attribute_t *)custom_attribute_it->data;
 		desc->custom_sdp_attributes = sal_custom_sdp_attribute_append(desc->custom_sdp_attributes, belle_sdp_attribute_get_name(attr), belle_sdp_attribute_get_value(attr));
+		
+		if (strcasecmp(belle_sdp_attribute_get_name(attr), "group") == 0){
+			value = belle_sdp_attribute_get_value(attr);
+			if (value && strncasecmp(value, "BUNDLE", strlen("BUNDLE")) == 0){
+				add_bundles(desc, value + strlen("BUNDLE"));
+			}
+		}
 	}
 
 	for ( media_desc_it=belle_sdp_session_description_get_media_descriptions ( session_desc )
diff --git a/coreapi/linphonecore.c b/coreapi/linphonecore.c
index 41142fff623f261524badee0ad6adcc407a4ae3a..ad15917a242bdd23471818c17a593a9b4d54b760 100644
--- a/coreapi/linphonecore.c
+++ b/coreapi/linphonecore.c
@@ -69,6 +69,8 @@
 #include "content/content-manager.h"
 #include "content/content-type.h"
 #include "core/core-p.h"
+#include "conference/session/media-session.h"
+#include "conference/session/media-session-p.h"
 
 // For migration purpose.
 #include "address/address-p.h"
@@ -2557,7 +2559,6 @@ static void linphone_core_init(LinphoneCore * lc, LinphoneCoreCbs *cbs, LpConfig
 	linphone_presence_model_set_basic_status(lc->presence_model, LinphonePresenceBasicStatusOpen);
 
 	_linphone_core_read_config(lc);
-	linphone_core_add_linphone_spec(lc, "ephemeral");
 	linphone_core_set_state(lc, LinphoneGlobalReady, "Ready");
 
 	if (automatically_start) {
@@ -5590,48 +5591,20 @@ void * linphone_core_get_native_video_window_id(const LinphoneCore *lc){
 #ifdef VIDEO_ENABLED
 		/*case where it was not set but we want to get the one automatically created by mediastreamer2 (desktop versions only)*/
 		LinphoneCall *call=linphone_core_get_current_call (lc);
+		
 		if (call) {
-			VideoStream *vstream = reinterpret_cast<VideoStream *>(linphone_call_get_stream(call, LinphoneStreamTypeVideo));
-			if (vstream)
-				return video_stream_get_native_window_id(vstream);
+			auto ms = dynamic_pointer_cast<LinphonePrivate::MediaSession>(L_GET_PRIVATE_FROM_C_OBJECT(call)->getActiveSession());
+			if (ms) return ms->getNativeVideoWindowId();
 		}
 #endif
 	}
 	return 0;
 }
 
-/* unsets the video id for all calls (indeed it may be kept by filters or videostream object itself by paused calls)*/
-static void unset_video_window_id(LinphoneCore *lc, bool_t preview, void *id){
-	if ((id != NULL)
-#ifndef _WIN32
-		&& ((unsigned long)id != (unsigned long)-1)
-#endif
-	){
-		ms_error("Invalid use of unset_video_window_id()");
-		return;
-	}
-	L_GET_PRIVATE_FROM_C_OBJECT(lc)->unsetVideoWindowId(!!preview, id);
-}
 
 void _linphone_core_set_native_video_window_id(LinphoneCore *lc, void *id) {
-	if ((id == NULL)
-#ifndef _WIN32
-		|| ((unsigned long)id == (unsigned long)-1)
-#endif
-	){
-		unset_video_window_id(lc,FALSE,id);
-	}
+	L_GET_PRIVATE_FROM_C_OBJECT(lc)->setVideoWindowId(false, id);
 	lc->video_window_id=id;
-#ifdef VIDEO_ENABLED
-	{
-		LinphoneCall *call=linphone_core_get_current_call(lc);
-		if (call) {
-			VideoStream *vstream = reinterpret_cast<VideoStream *>(linphone_call_get_stream(call, LinphoneStreamTypeVideo));
-			if (vstream)
-				video_stream_set_native_window_id(vstream,id);
-		}
-	}
-#endif
 }
 
 void linphone_core_set_native_video_window_id(LinphoneCore *lc, void *id) {
@@ -5650,10 +5623,10 @@ void * linphone_core_get_native_preview_window_id(const LinphoneCore *lc){
 		/*case where we want the id automatically created by mediastreamer2 (desktop versions only)*/
 #ifdef VIDEO_ENABLED
 		LinphoneCall *call=linphone_core_get_current_call(lc);
+		
 		if (call) {
-			VideoStream *vstream = reinterpret_cast<VideoStream *>(linphone_call_get_stream(call, LinphoneStreamTypeVideo));
-			if (vstream)
-				return video_stream_get_native_preview_window_id(vstream);
+			auto ms = dynamic_pointer_cast<LinphonePrivate::MediaSession>(L_GET_PRIVATE_FROM_C_OBJECT(call)->getActiveSession());
+			if (ms) return ms->getNativePreviewVideoWindowId();
 		}
 		if (lc->previewstream)
 			return video_preview_get_native_window_id(lc->previewstream);
@@ -5663,24 +5636,11 @@ void * linphone_core_get_native_preview_window_id(const LinphoneCore *lc){
 }
 
 void _linphone_core_set_native_preview_window_id(LinphoneCore *lc, void *id) {
-	if ((id == NULL)
-#ifndef _WIN32
-		|| ((unsigned long)id == (unsigned long)-1)
-#endif
-	) {
-		unset_video_window_id(lc,TRUE,id);
-	}
+	L_GET_PRIVATE_FROM_C_OBJECT(lc)->setVideoWindowId(true, id);
 	lc->preview_window_id=id;
 #ifdef VIDEO_ENABLED
-	{
-		LinphoneCall *call=linphone_core_get_current_call(lc);
-		if (call) {
-			VideoStream *vstream = reinterpret_cast<VideoStream *>(linphone_call_get_stream(call, LinphoneStreamTypeVideo));
-			if (vstream)
-				video_stream_set_native_preview_window_id(vstream,id);
-		}else if (lc->previewstream){
-			video_preview_set_native_window_id(lc->previewstream,id);
-		}
+	if (lc->previewstream){
+		video_preview_set_native_window_id(lc->previewstream,id);
 	}
 #endif
 }
@@ -6132,7 +6092,11 @@ void sip_config_uninit(LinphoneCore *lc)
 		for(elem=config->proxies;elem!=NULL;elem=bctbx_list_next(elem)){
 			LinphoneProxyConfig *cfg=(LinphoneProxyConfig*)(elem->data);
 			_linphone_proxy_config_unpublish(cfg);	/* to unpublish without changing the stored flag enable_publish */
-			_linphone_proxy_config_unregister(cfg);	/* to unregister without changing the stored flag enable_register */
+			
+			/* Do not unregister when push notifications are allowed, otherwise this clears tokens from the SIP server.*/
+			if (!linphone_proxy_config_is_push_notification_allowed(cfg)){
+				_linphone_proxy_config_unregister(cfg);	/* to unregister without changing the stored flag enable_register */
+			}
 		}
 
 		ms_message("Unregistration started.");
@@ -7146,7 +7110,7 @@ void linphone_core_set_media_encryption_mandatory(LinphoneCore *lc, bool_t m) {
 }
 
 void linphone_core_init_default_params(LinphoneCore*lc, LinphoneCallParams *params) {
-	L_GET_CPP_PTR_FROM_C_OBJECT(params)->initDefault(L_GET_CPP_PTR_FROM_C_OBJECT(lc));
+	L_GET_CPP_PTR_FROM_C_OBJECT(params)->initDefault(L_GET_CPP_PTR_FROM_C_OBJECT(lc), LinphoneCallOutgoing);
 }
 
 void linphone_core_set_device_identifier(LinphoneCore *lc,const char* device_id) {
@@ -7336,6 +7300,14 @@ bool_t linphone_core_video_multicast_enabled(const LinphoneCore *lc) {
 	return lc->rtp_conf.video_multicast_enabled;
 }
 
+bool_t linphone_core_rtp_bundle_enabled(const LinphoneCore *lc){
+	return linphone_config_get_bool(lc->config, "rtp", "bundle", FALSE);
+}
+
+void linphone_core_enable_rtp_bundle(LinphoneCore *lc, bool_t value){
+	linphone_config_set_bool(lc->config, "rtp", "bundle", value);
+}
+
 void linphone_core_set_video_preset(LinphoneCore *lc, const char *preset) {
 	lp_config_set_string(lc->config, "video", "preset", preset);
 }
diff --git a/coreapi/misc.c b/coreapi/misc.c
index bcf3082b4df51b69e66c5b7cf3f56f5147fe128e..c7815cca6082f9d2e354fd66d3093c3dad1ad570 100644
--- a/coreapi/misc.c
+++ b/coreapi/misc.c
@@ -45,10 +45,6 @@
 #undef snprintf
 #include <mediastreamer2/stun.h>
 
-#ifdef HAVE_GETIFADDRS
-#include <net/if.h>
-#include <ifaddrs.h>
-#endif
 #include <math.h>
 #if _MSC_VER
 #define snprintf _snprintf
diff --git a/coreapi/offeranswer.c b/coreapi/offeranswer.c
index 871361ab883d7731d6f049c9216026afbe7516f3..406ebe49e337e59086db517480b5d888013c3e41 100644
--- a/coreapi/offeranswer.c
+++ b/coreapi/offeranswer.c
@@ -336,7 +336,7 @@ static SalStreamDir compute_dir_incoming(SalStreamDir local, SalStreamDir offere
 static void initiate_outgoing(MSFactory* factory, const SalStreamDescription *local_offer,
 						const SalStreamDescription *remote_answer,
 						SalStreamDescription *result){
-	if (remote_answer->rtp_port!=0)
+	if (sal_stream_description_enabled(remote_answer))
 		result->payloads=match_payloads(factory, local_offer->payloads,remote_answer->payloads,TRUE,FALSE);
 	else {
 		ms_message("Local stream description [%p] rejected by peer",local_offer);
@@ -411,7 +411,18 @@ static void initiate_outgoing(MSFactory* factory, const SalStreamDescription *lo
 		result->dir=compute_dir_outgoing(local_offer->dir,remote_answer->dir);
 	}
 
-
+	if (remote_answer->mid[0] != '\0'){
+		if (local_offer->mid[0] != '\0'){
+			strncpy(result->mid, remote_answer->mid, sizeof(result->mid) - 1);
+			result->mid_rtp_ext_header_id = remote_answer->mid_rtp_ext_header_id;
+			result->bundle_only = remote_answer->bundle_only;
+			result->rtcp_mux = TRUE; /* RTCP mux must be enabled in bundle mode. */
+		}else{
+			ms_error("The remote has set a mid in an answer while we didn't offered it.");
+		}
+	}else{
+		result->rtcp_mux = remote_answer->rtcp_mux && local_offer->rtcp_mux;
+	}
 
 	if (result->payloads && !only_telephone_event(result->payloads)){
 		strcpy(result->rtp_addr,remote_answer->rtp_addr);
@@ -422,13 +433,14 @@ static void initiate_outgoing(MSFactory* factory, const SalStreamDescription *lo
 		result->ptime=remote_answer->ptime;
 		result->maxptime=remote_answer->maxptime;
 	}else{
-		result->rtp_port=0;
+		sal_stream_description_disable(result);
 	}
 	if (sal_stream_description_has_srtp(result) == TRUE) {
 		/* verify crypto algo */
 		memset(result->crypto, 0, sizeof(result->crypto));
-		if (!match_crypto_algo(local_offer->crypto, remote_answer->crypto, &result->crypto[0], &result->crypto_local_tag, FALSE))
-			result->rtp_port = 0;
+		if (!match_crypto_algo(local_offer->crypto, remote_answer->crypto, &result->crypto[0], &result->crypto_local_tag, FALSE)){
+			sal_stream_description_disable(result);
+		}
 	}
 	result->rtp_ssrc=local_offer->rtp_ssrc;
 	strncpy(result->rtcp_cname,local_offer->rtcp_cname,sizeof(result->rtcp_cname));
@@ -446,19 +458,19 @@ static void initiate_outgoing(MSFactory* factory, const SalStreamDescription *lo
 		result->dtls_fingerprint[0] = '\0';
 		result->dtls_role = SalDtlsRoleInvalid;
 	}
-	result->rtcp_mux = remote_answer->rtcp_mux && local_offer->rtcp_mux;
 	result->implicit_rtcp_fb = local_offer->implicit_rtcp_fb && remote_answer->implicit_rtcp_fb;
 }
 
 
 static void initiate_incoming(MSFactory *factory, const SalStreamDescription *local_cap,
 						const SalStreamDescription *remote_offer,
-						SalStreamDescription *result, bool_t one_matching_codec){
+						SalStreamDescription *result, bool_t one_matching_codec, const char *bundle_owner_mid){
 	result->payloads=match_payloads(factory, local_cap->payloads,remote_offer->payloads, FALSE, one_matching_codec);
 	result->proto=remote_offer->proto;
 	result->type=local_cap->type;
 	result->dir=compute_dir_incoming(local_cap->dir,remote_offer->dir);
-	if (!result->payloads || only_telephone_event(result->payloads) || remote_offer->rtp_port==0){
+	
+	if (!result->payloads || only_telephone_event(result->payloads) || !sal_stream_description_enabled(remote_offer)){
 		result->rtp_port=0;
 		return;
 	}
@@ -488,12 +500,28 @@ static void initiate_incoming(MSFactory *factory, const SalStreamDescription *lo
 		result->ptime=local_cap->ptime;
 		result->maxptime=local_cap->maxptime;
 	}
+	
+	/* Handle RTP bundle negociation */
+	if (remote_offer->mid[0] != '\0' && bundle_owner_mid){
+		strncpy(result->mid, remote_offer->mid, sizeof(result->mid) - 1);
+		result->mid_rtp_ext_header_id = remote_offer->mid_rtp_ext_header_id;
+		
+		if (strcmp(bundle_owner_mid, remote_offer->mid) != 0){
+			/* The stream is a secondary one part of a bundle.
+			 * In this case it must set the bundle-only attribute, and set port to zero.*/
+			result->bundle_only = TRUE;
+			result->rtp_port = 0;
+		}
+		result->rtcp_mux = TRUE; /* RTCP mux must be enabled in bundle mode. */
+	}else {
+		result->rtcp_mux = remote_offer->rtcp_mux && local_cap->rtcp_mux;
+	}
 
 	if (sal_stream_description_has_srtp(result) == TRUE) {
 		/* select crypto algo */
 		memset(result->crypto, 0, sizeof(result->crypto));
 		if (!match_crypto_algo(local_cap->crypto, remote_offer->crypto, &result->crypto[0], &result->crypto_local_tag, TRUE)) {
-			result->rtp_port = 0;
+			sal_stream_description_disable(result);
 			ms_message("No matching crypto algo for remote stream's offer [%p]",remote_offer);
 		}
 
@@ -528,7 +556,6 @@ static void initiate_incoming(MSFactory *factory, const SalStreamDescription *lo
 		result->dtls_fingerprint[0] = '\0';
 		result->dtls_role = SalDtlsRoleInvalid;
 	}
-	result->rtcp_mux = remote_offer->rtcp_mux && local_cap->rtcp_mux;
 	result->implicit_rtcp_fb = local_cap->implicit_rtcp_fb && remote_offer->implicit_rtcp_fb;
 }
 
@@ -567,6 +594,16 @@ int offer_answer_initiate_outgoing(MSFactory *factory, const SalMediaDescription
 	if ((local_offer->rtcp_xr.enabled == TRUE) && (remote_answer->rtcp_xr.enabled == FALSE)) {
 		result->rtcp_xr.enabled = FALSE;
 	}
+	/* TODO: check that the bundle answer is compliant with our offer.
+	 * For now, just check the presence of a bundle response. */
+	if (local_offer->bundles){
+		if (remote_answer->bundles){
+			/* Copy the bundle offering to the result media description. */
+			result->bundles = bctbx_list_copy_with_data(remote_answer->bundles, (bctbx_list_copy_func) sal_stream_bundle_clone);
+		}
+	}else if (remote_answer->bundles){
+		ms_error("Remote answerer is proposing bundles, which we did not offer.");
+	}
 
 	return 0;
 }
@@ -582,11 +619,23 @@ int offer_answer_initiate_incoming(MSFactory *factory, const SalMediaDescription
 	int i;
 	const SalStreamDescription *ls=NULL,*rs;
 
+	if (remote_offer->bundles && local_capabilities->accept_bundles){
+		/* Copy the bundle offering to the result media description. */
+		result->bundles = bctbx_list_copy_with_data(remote_offer->bundles, (bctbx_list_copy_func) sal_stream_bundle_clone);
+	}
+	
 	for(i=0;i<remote_offer->nb_streams;++i){
 		rs = &remote_offer->streams[i];
 		ls = &local_capabilities->streams[i];
 		if (ls && rs->type == ls->type && rs->proto == ls->proto){
-			initiate_incoming(factory, ls,rs,&result->streams[i],one_matching_codec);
+			const char *bundle_owner_mid = NULL;
+			if (local_capabilities->accept_bundles){
+				int owner_index = sal_media_description_get_index_of_transport_owner(remote_offer, rs);
+				if (owner_index != -1){
+					bundle_owner_mid = remote_offer->streams[owner_index].mid;
+				}
+			}
+			initiate_incoming(factory, ls,rs,&result->streams[i],one_matching_codec, bundle_owner_mid);
 			// Handle global RTCP FB attributes
 			result->streams[i].rtcp_fb.generic_nack_enabled = rs->rtcp_fb.generic_nack_enabled;
 			result->streams[i].rtcp_fb.tmmbr_enabled = rs->rtcp_fb.tmmbr_enabled;
diff --git a/coreapi/private_functions.h b/coreapi/private_functions.h
index 5384d3ce8ed35f011ec3b04702f93b86a75089a5..5d2ae99a83df43864bf68916438feb162cc0788a 100644
--- a/coreapi/private_functions.h
+++ b/coreapi/private_functions.h
@@ -240,8 +240,6 @@ LINPHONE_PUBLIC void linphone_core_enable_short_turn_refresh(LinphoneCore *lc, b
 LINPHONE_PUBLIC void linphone_call_stats_fill(LinphoneCallStats *stats, MediaStream *ms, OrtpEvent *ev);
 void linphone_call_stats_update(LinphoneCallStats *stats, MediaStream *stream);
 LinphoneCallStats *_linphone_call_stats_new(void);
-void _linphone_call_stats_uninit(LinphoneCallStats *stats);
-void _linphone_call_stats_clone(LinphoneCallStats *dst, const LinphoneCallStats *src);
 void _linphone_call_stats_set_ice_state (LinphoneCallStats *stats, LinphoneIceState state);
 void _linphone_call_stats_set_type (LinphoneCallStats *stats, LinphoneStreamType type);
 void _linphone_call_stats_set_received_rtcp (LinphoneCallStats *stats, mblk_t *m);
diff --git a/coreapi/private_structs.h b/coreapi/private_structs.h
index 0148991a0ad220889811bd490b7b6afd6efc3af8..5e6fbb49f3bd4c9e3407385771884ce4f350a652 100644
--- a/coreapi/private_structs.h
+++ b/coreapi/private_structs.h
@@ -31,8 +31,8 @@
 
 struct _LinphoneQualityReporting{
 	reporting_session_report_t * reports[3]; /**Store information on audio and video media streams (RFC 6035) */
-	bool_t was_video_running; /*Keep video state since last check in order to detect its (de)activation*/
 	LinphoneQualityReportingReportSendCb on_report_sent;
+	bool_t was_video_running; /*Keep video state since last check in order to detect its (de)activation*/
 };
 
 struct _LinphoneCallLog{
diff --git a/coreapi/proxy.c b/coreapi/proxy.c
index 35a36c38c1fe774521561b3ab34fe686ca679425..53d133fa9ec8add54a1e08e21cedf6466dba902d 100644
--- a/coreapi/proxy.c
+++ b/coreapi/proxy.c
@@ -165,7 +165,7 @@ static void linphone_proxy_config_init(LinphoneCore* lc, LinphoneProxyConfig *cf
 	cfg->avpf_rr_interval = lc ? !!lp_config_get_default_int(lc->config, "proxy", "avpf_rr_interval", 5) : 5;
 	cfg->publish_expires= lc ? lp_config_get_default_int(lc->config, "proxy", "publish_expires", -1) : -1;
 	cfg->publish = lc ? !!lp_config_get_default_int(lc->config, "proxy", "publish", FALSE) : FALSE;
-	cfg->push_notification_allowed = lc ? !!lp_config_get_default_int(lc->config, "proxy", "push_notification_allowed", TRUE) : TRUE;
+	cfg->push_notification_allowed = lc ? !!lp_config_get_default_int(lc->config, "proxy", "push_notification_allowed", FALSE) : FALSE;
 	cfg->refkey = refkey ? ms_strdup(refkey) : NULL;
 	if (nat_policy_ref) {
 		LinphoneNatPolicy *policy = linphone_config_create_nat_policy_from_section(lc->config,nat_policy_ref);
@@ -589,8 +589,7 @@ static LinphoneAddress *guess_contact_for_register (LinphoneProxyConfig *cfg) {
 
 void _linphone_proxy_config_unregister(LinphoneProxyConfig *obj) {
 	if (obj->op && (obj->state == LinphoneRegistrationOk ||
-					(obj->state == LinphoneRegistrationProgress && obj->expires != 0)) &&
-					!linphone_proxy_config_is_push_notification_allowed(obj)) {
+					(obj->state == LinphoneRegistrationProgress && obj->expires != 0))) {
 		obj->op->unregister();
 	}
 }
@@ -1815,6 +1814,7 @@ void linphone_proxy_config_set_conference_factory_uri(LinphoneProxyConfig *cfg,
 		cfg->conference_factory_uri = bctbx_strdup(uri);
 		if (cfg->lc) {
 			linphone_core_add_linphone_spec(cfg->lc, "groupchat");
+			linphone_core_add_linphone_spec(cfg->lc, "ephemeral");
 		}
 	} else if (cfg->lc) {
 		bool_t remove = TRUE;
@@ -1831,6 +1831,7 @@ void linphone_proxy_config_set_conference_factory_uri(LinphoneProxyConfig *cfg,
 		}
 		if (remove) {
 			linphone_core_remove_linphone_spec(cfg->lc, "groupchat");
+			linphone_core_remove_linphone_spec(cfg->lc, "ephemeral");
 		}
 	}
 }
diff --git a/coreapi/quality_reporting.c b/coreapi/quality_reporting.c
index 8e6973ce0af3e317577b26de867cdaf69e353012..3d036045ca38a4a2e86dcc4e933a89ee68e57d72 100644
--- a/coreapi/quality_reporting.c
+++ b/coreapi/quality_reporting.c
@@ -169,8 +169,11 @@ static bool_t media_report_enabled(LinphoneCall * call, int stats_type){
 	if (!quality_reporting_enabled(call))
 		return FALSE;
 
-	if (stats_type == LINPHONE_CALL_STATS_VIDEO && !linphone_call_params_video_enabled(linphone_call_get_current_params(call)))
-		return FALSE;
+	if (stats_type == LINPHONE_CALL_STATS_VIDEO){
+		if (!(L_GET_CPP_PTR_FROM_C_OBJECT(call)->getLog()->reporting.was_video_running
+			|| linphone_call_params_video_enabled(linphone_call_get_current_params(call))) )
+			return FALSE;
+	}
 
 	if (stats_type == LINPHONE_CALL_STATS_TEXT && !linphone_call_params_realtime_text_enabled(linphone_call_get_current_params(call)))
 		return FALSE;
@@ -413,7 +416,7 @@ static const SalStreamDescription * get_media_stream_for_desc(const SalMediaDesc
 	int count;
 	if (smd != NULL) {
 		for (count = 0; count < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; ++count) {
-			if (sal_stream_description_active(&smd->streams[count]) && smd->streams[count].type == sal_stream_type) {
+			if (sal_stream_description_enabled(&smd->streams[count]) && smd->streams[count].type == sal_stream_type) {
 				return &smd->streams[count];
 			}
 		}
@@ -480,7 +483,7 @@ static void qos_analyzer_on_action_suggested(void *user_data, int datac, const c
 		}
 	}
 
-	appendbuf=ms_strdup_printf("%s%d;", report->qos_analyzer.timestamp?report->qos_analyzer.timestamp:"", ms_time(0));
+	appendbuf=ms_strdup_printf("%s%llu;", report->qos_analyzer.timestamp?report->qos_analyzer.timestamp:"", (unsigned long long)ms_time(0));
 	STR_REASSIGN(report->qos_analyzer.timestamp,appendbuf);
 
 	STR_REASSIGN(report->qos_analyzer.input_leg, ms_strdup_printf("%s aenc_ptime aenc_br a_dbw a_ubw venc_br v_dbw v_ubw tenc_br t_dbw t_ubw", datav[0]));
@@ -741,10 +744,10 @@ void linphone_reporting_call_state_updated(LinphoneCall *call){
 				}
 			}
 			linphone_reporting_update_ip(call);
-			if (!media_report_enabled(call, LINPHONE_CALL_STATS_VIDEO) && log->reporting.was_video_running){
+			if (media_report_enabled(call, LINPHONE_CALL_STATS_VIDEO) && log->reporting.was_video_running){
 				send_report(call, log->reporting.reports[LINPHONE_CALL_STATS_VIDEO], "VQSessionReport");
 			}
-			log->reporting.was_video_running=media_report_enabled(call, LINPHONE_CALL_STATS_VIDEO);
+			log->reporting.was_video_running = linphone_call_params_video_enabled(linphone_call_get_current_params(call));
 			break;
 		}
 		case LinphoneCallEnd:{
diff --git a/coreapi/tester_utils.h b/coreapi/tester_utils.h
index c8037577dad1599b45d2be141b3e7dd8e7d9c909..7b06acc49261c45727e522753536019d2dbfc948 100644
--- a/coreapi/tester_utils.h
+++ b/coreapi/tester_utils.h
@@ -112,6 +112,8 @@ LINPHONE_PUBLIC bool_t linphone_call_params_get_update_call_when_ice_completed(c
 LINPHONE_PUBLIC int _linphone_call_stats_get_updated(const LinphoneCallStats *stats);
 LINPHONE_PUBLIC bool_t _linphone_call_stats_rtcp_received_via_mux(const LinphoneCallStats *stats);
 LINPHONE_PUBLIC mblk_t *_linphone_call_stats_get_received_rtcp (const LinphoneCallStats *stats);
+LINPHONE_PUBLIC bool_t _linphone_call_stats_has_received_rtcp(const LinphoneCallStats *stats);
+LINPHONE_PUBLIC bool_t _linphone_call_stats_has_sent_rtcp(const LinphoneCallStats *stats);
 
 LINPHONE_PUBLIC LinphoneQualityReporting *linphone_call_log_get_quality_reporting(LinphoneCallLog *call_log);
 LINPHONE_PUBLIC reporting_session_report_t **linphone_quality_reporting_get_reports(LinphoneQualityReporting *qreporting);
@@ -214,6 +216,8 @@ LINPHONE_PUBLIC void linphone_account_creator_cbs_set_confirmation_key(LinphoneA
 LINPHONE_PUBLIC void linphone_core_delete_local_encryption_db(const LinphoneCore *lc);
 LINPHONE_PUBLIC void linphone_core_set_network_reachable_internal(LinphoneCore *lc, bool_t is_reachable);
 
+LINPHONE_PUBLIC bctbx_list_t *linphone_fetch_local_addresses(void);
+
 #ifndef __cplusplus
 LINPHONE_PUBLIC Sal *linphone_core_get_sal(const LinphoneCore *lc);
 LINPHONE_PUBLIC SalOp *linphone_proxy_config_get_sal_op(const LinphoneProxyConfig *cfg);
diff --git a/include/linphone/call_params.h b/include/linphone/call_params.h
index 1f29a2da86139de2d876f955d3bd53a522fbc5e2..55381ee8a748d5e3615f9982ccc1c18b51ff76cb 100644
--- a/include/linphone/call_params.h
+++ b/include/linphone/call_params.h
@@ -520,6 +520,27 @@ LINPHONE_PUBLIC bctbx_list_t* linphone_call_params_get_custom_contents (const Li
 **/
 LINPHONE_PUBLIC void linphone_call_params_add_custom_content (LinphoneCallParams *params, LinphoneContent *content);
 
+/**
+ * Indicates whether RTP bundle mode (also known as Media Multiplexing) is enabled.
+ * See https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-54 for more information.
+ * @param[in] params the #LinphoneCallParams
+ * @return a boolean indicating the enablement of rtp bundle mode.
+ * @ingroup media_parameters
+ */
+LINPHONE_PUBLIC bool_t linphone_call_params_rtp_bundle_enabled(const LinphoneCallParams *params);
+
+/**
+ * Enables or disables RTP bundle mode (Media Multiplexing).
+ * See https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-54 for more information about the feature.
+ * When enabled, liblinphone will try to negociate the use of a single port for all streams.
+ * It automatically enables rtcp-mux.
+ * @param[in] params the #LinphoneCallParams
+ * @param[in] value a boolean to indicate whether the feature is to be enabled.
+ * @ingroup media_parameters
+ */
+LINPHONE_PUBLIC void linphone_call_params_enable_rtp_bundle(LinphoneCallParams *params, bool_t value);
+
+
 /*******************************************************************************
  * DEPRECATED                                                                  *
  ******************************************************************************/
diff --git a/include/linphone/core.h b/include/linphone/core.h
index 692002be9f3b08fb10027c685bec3494803252e7..2c08fb6348bbf300180262de90463be2cd028b1e 100644
--- a/include/linphone/core.h
+++ b/include/linphone/core.h
@@ -5151,6 +5151,27 @@ LINPHONE_PUBLIC void linphone_core_enable_video_multicast(LinphoneCore *core, bo
 **/
 LINPHONE_PUBLIC bool_t linphone_core_video_multicast_enabled(const LinphoneCore *core);
 
+/**
+ * Returns whether RTP bundle mode (also known as Media Multiplexing) is enabled.
+ * See https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-54 for more information.
+ * @param[in] lc the #LinphoneCore
+ * @return a boolean indicating the enablement of rtp bundle mode.
+ * @ingroup media_parameters
+ */
+LINPHONE_PUBLIC bool_t linphone_core_rtp_bundle_enabled(const LinphoneCore *lc);
+
+/**
+ * Enables or disables RTP bundle mode (Media Multiplexing).
+ * See https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-54 for more information about the feature.
+ * When enabled, liblinphone will try to negociate the use of a single port for all streams when doing an outgoing call.
+ * It automatically enables rtcp-mux.
+ * This feature can also be enabled per-call using #LinphoneCallParams.
+ * @param[in] lc the #LinphoneCore
+ * @param[in] value a boolean to indicate whether the feature is to be enabled.
+ * @ingroup media_parameters
+ */
+LINPHONE_PUBLIC void linphone_core_enable_rtp_bundle(LinphoneCore *lc, bool_t value);
+
 /**
  * @brief Set the network simulator parameters.
  *
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c4038e576657d82355b0dfb72d8bcac15c10bde0..bfb14125994e427e343872800b4f72ae49766f38 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -164,8 +164,11 @@ set(LINPHONE_CXX_OBJECTS_PRIVATE_HEADER_FILES
 	conference/session/call-session-p.h
 	conference/session/call-session.h
 	conference/session/media-session.h
+	conference/session/streams.h
 	conference/session/port-config.h
 	conference/session/tone-manager.h
+	conference/session/ms2-streams.h
+	conference/session/media-description-renderer.h
 	containers/lru-cache.h
 	content/content-disposition.h
 	content/content-manager.h
@@ -210,7 +213,7 @@ set(LINPHONE_CXX_OBJECTS_PRIVATE_HEADER_FILES
 	event-log/events.h
 	hacks/hacks.h
 	logger/logger.h
-	nat/ice-agent.h
+	nat/ice-service.h
 	nat/stun-client.h
 	object/app-data-container.h
 	object/base-object-p.h
@@ -231,6 +234,7 @@ set(LINPHONE_CXX_OBJECTS_PRIVATE_HEADER_FILES
 	utils/background-task.h
 	utils/general-internal.h
 	utils/payload-type-handler.h
+	utils/if-addrs.h
 	variant/variant.h
 )
 
@@ -333,6 +337,14 @@ set(LINPHONE_CXX_OBJECTS_SOURCE_FILES
 	conference/session/call-session.cpp
 	conference/session/media-session.cpp
 	conference/session/tone-manager.cpp
+	conference/session/media-description-renderer.cpp
+	conference/session/stream.cpp
+	conference/session/streams-group.cpp
+	conference/session/ms2-stream.cpp
+	conference/session/audio-stream.cpp
+	conference/session/video-stream.cpp
+	conference/session/rtt-stream.cpp
+	conference/session/media-session.cpp
 	content/content-disposition.cpp
 	content/content-manager.cpp
 	content/content-type.cpp
@@ -366,7 +378,7 @@ set(LINPHONE_CXX_OBJECTS_SOURCE_FILES
 	event-log/event-log.cpp
 	hacks/hacks.cpp
 	logger/logger.cpp
-	nat/ice-agent.cpp
+	nat/ice-service.cpp
 	nat/stun-client.cpp
 	object/app-data-container.cpp
 	object/base-object.cpp
@@ -388,6 +400,7 @@ set(LINPHONE_CXX_OBJECTS_SOURCE_FILES
 	utils/general.cpp
 	utils/payload-type-handler.cpp
 	utils/utils.cpp
+	utils/if-addrs.cpp
 	variant/variant.cpp
 )
 
diff --git a/src/c-wrapper/api/c-call-params.cpp b/src/c-wrapper/api/c-call-params.cpp
index d6b0fe669299d55f64d907c5100f00833b513d51..03983c9a7a0f003e9c323eb53b65f7dd742c129b 100644
--- a/src/c-wrapper/api/c-call-params.cpp
+++ b/src/c-wrapper/api/c-call-params.cpp
@@ -508,6 +508,14 @@ void linphone_call_params_add_custom_content (LinphoneCallParams *params, Linpho
 	L_GET_CPP_PTR_FROM_C_OBJECT(params)->addCustomContent(*cppContent);
 }
 
+bool_t linphone_call_params_rtp_bundle_enabled(const LinphoneCallParams *params){
+	return (bool_t)L_GET_CPP_PTR_FROM_C_OBJECT(params)->rtpBundleEnabled();
+}
+
+void linphone_call_params_enable_rtp_bundle(LinphoneCallParams *params, bool_t value){
+	L_GET_CPP_PTR_FROM_C_OBJECT(params)->enableRtpBundle(!!value);
+}
+
 // =============================================================================
 // Reference and user data handling functions.
 // =============================================================================
@@ -536,7 +544,7 @@ void linphone_call_params_unref (LinphoneCallParams *cp) {
 LinphoneCallParams *linphone_call_params_new (LinphoneCore *core) {
 	LinphoneCallParams *params = L_INIT(CallParams);
 	L_SET_CPP_PTR_FROM_C_OBJECT(params, new LinphonePrivate::MediaSessionParams());
-	L_GET_CPP_PTR_FROM_C_OBJECT(params)->initDefault(L_GET_CPP_PTR_FROM_C_OBJECT(core));
+	L_GET_CPP_PTR_FROM_C_OBJECT(params)->initDefault(L_GET_CPP_PTR_FROM_C_OBJECT(core), LinphoneCallOutgoing);
 	return params;
 }
 
diff --git a/src/c-wrapper/api/c-call-stats.cpp b/src/c-wrapper/api/c-call-stats.cpp
index 45f32311dba6fad848a2c97a6236118674e49aa7..88ef08cfccaf5786b508ae22e05c3bce4aafc7f8 100644
--- a/src/c-wrapper/api/c-call-stats.cpp
+++ b/src/c-wrapper/api/c-call-stats.cpp
@@ -23,7 +23,8 @@
 
 // =============================================================================
 
-void _linphone_call_stats_clone (LinphoneCallStats *dst, const LinphoneCallStats *src);
+static void _linphone_call_stats_clone (LinphoneCallStats *dst, const LinphoneCallStats *src);
+static void _linphone_call_stats_uninit (LinphoneCallStats *stats);
 
 /**
  * The LinphoneCallStats objects carries various statistic informations regarding quality of audio or video streams.
@@ -53,8 +54,8 @@ struct _LinphoneCallStats {
 	rtp_stats_t rtp_stats; /**< RTP stats */
 	int rtp_remote_family; /**< Ip adress family of the remote destination */
 	int clockrate;  /*RTP clockrate of the stream, provided here for easily converting timestamp units expressed in RTCP packets in milliseconds*/
-	bool_t rtcp_received_via_mux; /*private flag, for non-regression test only*/
 	float estimated_download_bandwidth; /**<Estimated download bandwidth measurement of received stream, expressed in kbit/s, including IP/UDP/RTP headers*/
+	bool_t rtcp_received_via_mux; /*private flag, for non-regression test only*/
 };
 
 BELLE_SIP_DECLARE_VPTR_NO_EXPORT(LinphoneCallStats);
@@ -62,7 +63,7 @@ BELLE_SIP_DECLARE_VPTR_NO_EXPORT(LinphoneCallStats);
 BELLE_SIP_DECLARE_NO_IMPLEMENTED_INTERFACES(LinphoneCallStats);
 
 BELLE_SIP_INSTANCIATE_VPTR(LinphoneCallStats, belle_sip_object_t,
-	NULL, // destroy
+	_linphone_call_stats_uninit, // destroy
 	_linphone_call_stats_clone, // clone
 	NULL, // marshal
 	FALSE
@@ -77,7 +78,7 @@ LinphoneCallStats *_linphone_call_stats_new () {
 	return stats;
 }
 
-void _linphone_call_stats_uninit (LinphoneCallStats *stats) {
+static void _linphone_call_stats_uninit (LinphoneCallStats *stats) {
 	if (stats->received_rtcp) {
 		freemsg(stats->received_rtcp);
 		stats->received_rtcp=NULL;
@@ -88,16 +89,26 @@ void _linphone_call_stats_uninit (LinphoneCallStats *stats) {
 	}
 }
 
-void _linphone_call_stats_clone (LinphoneCallStats *dst, const LinphoneCallStats *src) {
-	/*
-	 * Save the belle_sip_object_t part, copy the entire structure and restore the belle_sip_object_t part
-	 */
-	belle_sip_object_t tmp = dst->base;
-	memcpy(dst, src, sizeof(LinphoneCallStats));
-	dst->base = tmp;
-
-	dst->received_rtcp = NULL;
-	dst->sent_rtcp = NULL;
+static void _linphone_call_stats_clone (LinphoneCallStats *dst, const LinphoneCallStats *src) {
+	dst->type = src->type;
+	dst->jitter_stats = src->jitter_stats;
+	dst->received_rtcp = src->received_rtcp ? dupmsg(src->received_rtcp) : nullptr;
+	dst->sent_rtcp = src->sent_rtcp ? dupmsg(src->sent_rtcp) : nullptr;
+	dst->round_trip_delay = src->round_trip_delay;
+	dst->ice_state = src->ice_state;
+	dst->upnp_state = src->upnp_state;
+	dst->download_bandwidth = src->download_bandwidth;
+	dst->upload_bandwidth = src->upload_bandwidth;
+	dst->local_late_rate = src->local_late_rate;
+	dst->local_loss_rate = src->local_loss_rate;
+	dst->updated = src->updated;
+	dst->rtcp_download_bandwidth = src->rtcp_download_bandwidth;
+	dst->rtcp_upload_bandwidth = src->rtcp_upload_bandwidth;
+	dst->rtp_stats = src->rtp_stats;
+	dst->rtp_remote_family = src->rtp_remote_family;
+	dst->clockrate = src->clockrate;
+	dst->rtcp_received_via_mux = src->rtcp_received_via_mux;
+	dst->estimated_download_bandwidth = src->estimated_download_bandwidth;
 }
 
 void _linphone_call_stats_set_ice_state (LinphoneCallStats *stats, LinphoneIceState state) {
@@ -160,6 +171,14 @@ bool_t _linphone_call_stats_rtcp_received_via_mux (const LinphoneCallStats *stat
 	return stats->rtcp_received_via_mux;
 }
 
+bool_t _linphone_call_stats_has_received_rtcp(const LinphoneCallStats *stats){
+	return stats->received_rtcp != NULL;
+}
+
+bool_t _linphone_call_stats_has_sent_rtcp(const LinphoneCallStats *stats){
+	return stats->sent_rtcp != NULL;
+}
+
 // =============================================================================
 // Public functions
 // =============================================================================
@@ -226,8 +245,10 @@ LinphoneStreamType linphone_call_stats_get_type (const LinphoneCallStats *stats)
 float linphone_call_stats_get_sender_loss_rate (const LinphoneCallStats *stats) {
 	const report_block_t *srb = NULL;
 
-	if (!stats || !stats->sent_rtcp)
+	if (!stats->sent_rtcp){
+		ms_warning("linphone_call_stats_get_sender_loss_rate(): there is no RTCP packet sent.");
 		return 0.0;
+	}
 	/* Perform msgpullup() to prevent crashes in rtcp_is_SR() or rtcp_is_RR() if the RTCP packet is composed of several mblk_t structure */
 	if (stats->sent_rtcp->b_cont != NULL)
 		msgpullup(stats->sent_rtcp, (size_t)-1);
@@ -248,8 +269,10 @@ float linphone_call_stats_get_sender_loss_rate (const LinphoneCallStats *stats)
 float linphone_call_stats_get_receiver_loss_rate (const LinphoneCallStats *stats) {
 	const report_block_t *rrb = NULL;
 
-	if (!stats || !stats->received_rtcp)
+	if (!stats->received_rtcp){
+		ms_warning("linphone_call_stats_get_receiver_loss_rate(): there is no RTCP packet received.");
 		return 0.0;
+	}
 	/* Perform msgpullup() to prevent crashes in rtcp_is_SR() or rtcp_is_RR() if the RTCP packet is composed of several mblk_t structure */
 	if (stats->received_rtcp->b_cont != NULL)
 		msgpullup(stats->received_rtcp, (size_t)-1);
@@ -278,8 +301,10 @@ float linphone_call_stats_get_local_late_rate (const LinphoneCallStats *stats) {
 float linphone_call_stats_get_sender_interarrival_jitter (const LinphoneCallStats *stats) {
 	const report_block_t *srb = NULL;
 
-	if (!stats || !stats->sent_rtcp)
+	if (!stats->sent_rtcp){
+		ms_warning("linphone_call_stats_get_sender_interarrival_jitter(): there is no RTCP packet sent.");
 		return 0.0;
+	}
 	/* Perform msgpullup() to prevent crashes in rtcp_is_SR() or rtcp_is_RR() if the RTCP packet is composed of several mblk_t structure */
 	if (stats->sent_rtcp->b_cont != NULL)
 		msgpullup(stats->sent_rtcp, (size_t)-1);
@@ -297,8 +322,10 @@ float linphone_call_stats_get_sender_interarrival_jitter (const LinphoneCallStat
 float linphone_call_stats_get_receiver_interarrival_jitter (const LinphoneCallStats *stats) {
 	const report_block_t *rrb = NULL;
 
-	if (!stats || !stats->received_rtcp)
+	if (!stats->received_rtcp){
+		ms_warning("linphone_call_stats_get_receiver_interarrival_jitter(): there is no RTCP packet received.");
 		return 0.0;
+	}
 	/* Perform msgpullup() to prevent crashes in rtcp_is_SR() or rtcp_is_RR() if the RTCP packet is composed of several mblk_t structure */
 	if (stats->received_rtcp->b_cont != NULL)
 		msgpullup(stats->received_rtcp, (size_t)-1);
@@ -364,3 +391,4 @@ float linphone_call_stats_get_estimated_download_bandwidth(const LinphoneCallSta
 void linphone_call_stats_set_estimated_download_bandwidth(LinphoneCallStats *stats, float estimated_value) {
 	stats->estimated_download_bandwidth = estimated_value;
 }
+
diff --git a/src/c-wrapper/api/c-call.cpp b/src/c-wrapper/api/c-call.cpp
index 102244410c25480f9062734daab57c5916ac5ab1..d09bc90e14612254dffd74ca90aa52a949fc70a1 100644
--- a/src/c-wrapper/api/c-call.cpp
+++ b/src/c-wrapper/api/c-call.cpp
@@ -29,6 +29,7 @@
 #include "call/remote-conference-call.h"
 #include "chat/chat-room/real-time-text-chat-room.h"
 #include "conference/params/media-session-params-p.h"
+#include "conference/session/ms2-streams.h"
 #include "core/core-p.h"
 
 // =============================================================================
@@ -77,7 +78,7 @@ void linphone_call_init_media_streams (LinphoneCall *call) {
 
 /*This function is not static because used internally in linphone-daemon project*/
 void _post_configure_audio_stream (AudioStream *st, LinphoneCore *lc, bool_t muted) {
-	L_GET_PRIVATE_FROM_C_OBJECT(lc)->postConfigureAudioStream(st, !!muted);
+	LinphonePrivate::MS2AudioStream::postConfigureAudioStream(st, lc, !!muted);
 }
 
 void linphone_call_stop_media_streams (LinphoneCall *call) {
diff --git a/src/c-wrapper/internal/c-sal.cpp b/src/c-wrapper/internal/c-sal.cpp
index e82bb534934d08545b26cd5becfb7f8265d80a0b..8f6397e9e1ce3bd007733b4b638c6e1927bd4070 100644
--- a/src/c-wrapper/internal/c-sal.cpp
+++ b/src/c-wrapper/internal/c-sal.cpp
@@ -66,6 +66,28 @@ SalTransport sal_transport_parse(const char* param) {
 	return SalTransportUDP;
 }
 
+
+SalStreamBundle *sal_stream_bundle_new(void){
+	return ms_new0(SalStreamBundle, 1);
+}
+
+void sal_stream_bundle_add_stream(SalStreamBundle *bundle, SalStreamDescription *stream, const char *mid){
+	strncpy(stream->mid, mid ? mid : "", sizeof(stream->mid));
+	stream->mid[sizeof(stream->mid) -1] = '\0';
+	bundle->mids = bctbx_list_append(bundle->mids, ms_strdup(mid));
+}
+
+void sal_stream_bundle_destroy(SalStreamBundle *bundle){
+	bctbx_list_free_with_data(bundle->mids, (void (*)(void*)) ms_free);
+	ms_free(bundle);
+}
+
+SalStreamBundle *sal_stream_bundle_clone(const SalStreamBundle *bundle){
+	SalStreamBundle *ret = sal_stream_bundle_new();
+	ret->mids = bctbx_list_copy_with_data(bundle->mids, (bctbx_list_copy_func)bctbx_strdup);
+	return ret;
+}
+
 SalMediaDescription *sal_media_description_new(){
 	SalMediaDescription *md=ms_new0(SalMediaDescription,1);
 	int i;
@@ -79,6 +101,65 @@ SalMediaDescription *sal_media_description_new(){
 	return md;
 }
 
+SalStreamBundle * sal_media_description_add_new_bundle(SalMediaDescription *md){
+	SalStreamBundle *bundle = sal_stream_bundle_new();
+	md->bundles = bctbx_list_append(md->bundles, bundle);
+	return bundle;
+}
+
+int sal_stream_bundle_has_mid(const SalStreamBundle *bundle, const char *mid){
+	const bctbx_list_t *elem;
+	for (elem = bundle->mids; elem != NULL; elem = elem->next){
+		const char *m = (const char *) elem->data;
+		if (strcmp(m, mid) == 0) return TRUE;
+	}
+	return FALSE;
+}
+
+
+int sal_media_description_lookup_mid(const SalMediaDescription *md, const char *mid){
+	int index;
+	for (index = 0 ; index < md->nb_streams; ++index){
+		const SalStreamDescription * sd = &md->streams[index];
+		if (strcmp(sd->mid, mid) == 0){
+			return index;
+		}
+	}
+	return -1;
+}
+
+const SalStreamBundle *sal_media_description_get_bundle_from_mid(const SalMediaDescription *md, const char *mid){
+	const bctbx_list_t *elem;
+	for (elem = md->bundles; elem != NULL; elem = elem->next){
+		SalStreamBundle *bundle = (SalStreamBundle *)elem->data;
+		if (sal_stream_bundle_has_mid(bundle, mid)) return bundle;
+	}
+	return NULL;
+}
+
+const char *sal_stream_bundle_get_mid_of_transport_owner(const SalStreamBundle *bundle){
+	return (const char*)bundle->mids->data; /* the first one is the transport owner*/
+}
+
+int sal_media_description_get_index_of_transport_owner(const SalMediaDescription *md, const SalStreamDescription *sd){
+	const SalStreamBundle *bundle;
+	const char *master_mid;
+	int index;
+	if (sd->mid[0] == '\0') return -1; /* not part of any bundle */
+	/* lookup the mid in the bundle descriptions */
+	bundle = sal_media_description_get_bundle_from_mid(md, sd->mid);
+	if (!bundle) {
+		ms_warning("Orphan stream with mid '%s'", sd->mid);
+		return -1;
+	}
+	master_mid = sal_stream_bundle_get_mid_of_transport_owner(bundle);
+	index = sal_media_description_lookup_mid(md, master_mid);
+	if (index == -1){
+		ms_error("Stream with mid '%s' has no transport owner (mid '%s') !", sd->mid, master_mid);
+	}
+	return index;
+}
+
 static void sal_media_description_destroy(SalMediaDescription *md){
 	int i;
 	for(i=0;i<SAL_MEDIA_DESCRIPTION_MAX_STREAMS;i++){
@@ -88,6 +169,7 @@ static void sal_media_description_destroy(SalMediaDescription *md){
 		md->streams[i].already_assigned_payloads=NULL;
 		sal_custom_sdp_attribute_free(md->streams[i].custom_sdp_attributes);
 	}
+	bctbx_list_free_with_data(md->bundles, (void (*)(void*)) sal_stream_bundle_destroy);
 	sal_custom_sdp_attribute_free(md->custom_sdp_attributes);
 	ms_free(md);
 }
@@ -108,7 +190,7 @@ SalStreamDescription *sal_media_description_find_stream(SalMediaDescription *md,
 	int i;
 	for(i=0;i<SAL_MEDIA_DESCRIPTION_MAX_STREAMS;++i){
 		SalStreamDescription *ss=&md->streams[i];
-		if (!sal_stream_description_active(ss)) continue;
+		if (!sal_stream_description_enabled(ss)) continue;
 		if (ss->proto==proto && ss->type==type) return ss;
 	}
 	return NULL;
@@ -118,7 +200,7 @@ unsigned int sal_media_description_nb_active_streams_of_type(SalMediaDescription
 	unsigned int i;
 	unsigned int nb = 0;
 	for (i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; ++i) {
-		if (!sal_stream_description_active(&md->streams[i])) continue;
+		if (!sal_stream_description_enabled(&md->streams[i])) continue;
 		if (md->streams[i].type == type) nb++;
 	}
 	return nb;
@@ -127,7 +209,7 @@ unsigned int sal_media_description_nb_active_streams_of_type(SalMediaDescription
 SalStreamDescription * sal_media_description_get_active_stream_of_type(SalMediaDescription *md, SalStreamType type, unsigned int idx) {
 	unsigned int i;
 	for (i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; ++i) {
-		if (!sal_stream_description_active(&md->streams[i])) continue;
+		if (!sal_stream_description_enabled(&md->streams[i])) continue;
 		if (md->streams[i].type == type) {
 			if (idx-- == 0) return &md->streams[i];
 		}
@@ -160,7 +242,7 @@ void sal_media_description_set_dir(SalMediaDescription *md, SalStreamDir stream_
 	int i;
 	for(i=0;i<SAL_MEDIA_DESCRIPTION_MAX_STREAMS;++i){
 		SalStreamDescription *ss=&md->streams[i];
-		if (!sal_stream_description_active(ss)) continue;
+		if (!sal_stream_description_enabled(ss)) continue;
 		ss->dir=stream_dir;
 	}
 }
@@ -169,7 +251,7 @@ int sal_media_description_get_nb_active_streams(const SalMediaDescription *md) {
 	int i;
 	int nb = 0;
 	for (i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (sal_stream_description_active(&md->streams[i])) nb++;
+		if (sal_stream_description_enabled(&md->streams[i])) nb++;
 	}
 	return nb;
 }
@@ -185,7 +267,7 @@ static bool_t has_dir(const SalMediaDescription *md, SalStreamDir stream_dir){
 	/* we are looking for at least one stream with requested direction, inactive streams are ignored*/
 	for(i=0;i<SAL_MEDIA_DESCRIPTION_MAX_STREAMS;++i){
 		const SalStreamDescription *ss=&md->streams[i];
-		if (!sal_stream_description_active(ss)) continue;
+		if (!sal_stream_description_enabled(ss)) continue;
 		if (ss->dir==stream_dir) {
 			return TRUE;
 		}
@@ -213,8 +295,16 @@ bool_t sal_media_description_has_dir(const SalMediaDescription *md, SalStreamDir
 	return FALSE;
 }
 
-bool_t sal_stream_description_active(const SalStreamDescription *sd) {
-	return (sd->rtp_port > 0);
+bool_t sal_stream_description_enabled(const SalStreamDescription *sd) {
+	/* When the bundle-only attribute is present, a 0 rtp port doesn't mean that the stream is disabled.*/
+	return sd->rtp_port > 0 || sd->bundle_only;
+}
+
+void sal_stream_description_disable(SalStreamDescription *sd){
+	sd->rtp_port = 0;
+	/* Remove potential bundle parameters. A disabled stream is moved out of the bundle. */
+	sd->mid[0] = '\0';
+	sd->bundle_only = FALSE;
 }
 
 /*these are switch case, so that when a new proto is added we can't forget to modify this function*/
@@ -280,7 +370,7 @@ bool_t sal_media_description_has_avpf(const SalMediaDescription *md) {
 	int i;
 	if (md->nb_streams == 0) return FALSE;
 	for (i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&md->streams[i])) continue;
+		if (!sal_stream_description_enabled(&md->streams[i])) continue;
 		if (sal_stream_description_has_avpf(&md->streams[i]) != TRUE) return FALSE;
 	}
 	return TRUE;
@@ -290,7 +380,7 @@ bool_t sal_media_description_has_implicit_avpf(const SalMediaDescription *md) {
     int i;
     if (md->nb_streams == 0) return FALSE;
     for (i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-        if (!sal_stream_description_active(&md->streams[i])) continue;
+        if (!sal_stream_description_enabled(&md->streams[i])) continue;
         if (sal_stream_description_has_implicit_avpf(&md->streams[i]) != TRUE) return FALSE;
     }
     return TRUE;
@@ -300,17 +390,17 @@ bool_t sal_media_description_has_srtp(const SalMediaDescription *md) {
 	int i;
 	if (md->nb_streams == 0) return FALSE;
 	for (i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&md->streams[i])) continue;
-		if (sal_stream_description_has_srtp(&md->streams[i]) != TRUE) return FALSE;
+		if (!sal_stream_description_enabled(&md->streams[i])) continue;
+		if (sal_stream_description_has_srtp(&md->streams[i])) return TRUE;
 	}
-	return TRUE;
+	return FALSE;
 }
 
 bool_t sal_media_description_has_dtls(const SalMediaDescription *md) {
 	int i;
 	if (md->nb_streams == 0) return FALSE;
 	for (i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&md->streams[i])) continue;
+		if (!sal_stream_description_enabled(&md->streams[i])) continue;
 		if (sal_stream_description_has_dtls(&md->streams[i]) != TRUE) return FALSE;
 	}
 	return TRUE;
@@ -320,7 +410,7 @@ bool_t sal_media_description_has_zrtp(const SalMediaDescription *md) {
 	int i;
 	if (md->nb_streams == 0) return FALSE;
 	for (i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&md->streams[i])) continue;
+		if (!sal_stream_description_enabled(&md->streams[i])) continue;
 		if (sal_stream_description_has_zrtp(&md->streams[i]) != TRUE) return FALSE;
 	}
 	return TRUE;
@@ -330,7 +420,7 @@ bool_t sal_media_description_has_ipv6(const SalMediaDescription *md){
 	int i;
 	if (md->nb_streams == 0) return FALSE;
 	for (i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&md->streams[i])) continue;
+		if (!sal_stream_description_enabled(&md->streams[i])) continue;
 		if (md->streams[i].rtp_addr[0] != '\0'){
 			if (!sal_stream_description_has_ipv6(&md->streams[i])) return FALSE;
 		}else{
@@ -411,6 +501,7 @@ int sal_stream_description_equals(const SalStreamDescription *sd1, const SalStre
 	if (strcmp(sd1->rtp_addr, sd2->rtp_addr) != 0) result |= SAL_MEDIA_DESCRIPTION_NETWORK_CHANGED;
 	if (sd1->rtp_addr[0]!='\0' && sd2->rtp_addr[0]!='\0' && ms_is_multicast(sd1->rtp_addr) != ms_is_multicast(sd2->rtp_addr))
 			result |= SAL_MEDIA_DESCRIPTION_NETWORK_XXXCAST_CHANGED;
+	if (sd1->multicast_role != sd2->multicast_role) result |= SAL_MEDIA_DESCRIPTION_NETWORK_XXXCAST_CHANGED;
 	if (sd1->rtp_port != sd2->rtp_port) {
 		if ((sd1->rtp_port == 0) || (sd2->rtp_port == 0)) result |= SAL_MEDIA_DESCRIPTION_CODEC_CHANGED;
 		else result |= SAL_MEDIA_DESCRIPTION_NETWORK_CHANGED;
@@ -480,7 +571,7 @@ int sal_media_description_equals(const SalMediaDescription *md1, const SalMediaD
 	int i;
 
 	for(i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; ++i){
-		if (!sal_stream_description_active(&md1->streams[i]) && !sal_stream_description_active(&md2->streams[i])) continue;
+		if (!sal_stream_description_enabled(&md1->streams[i]) && !sal_stream_description_enabled(&md2->streams[i])) continue;
 		result |= sal_stream_description_equals(&md1->streams[i], &md2->streams[i]);
 	}
 	return result;
diff --git a/src/c-wrapper/internal/c-sal.h b/src/c-wrapper/internal/c-sal.h
index 998d9ed54b8da4ec7688722777f81893df710d07..98c542d63d146b66565ba080956e8ef13a7119bb 100644
--- a/src/c-wrapper/internal/c-sal.h
+++ b/src/c-wrapper/internal/c-sal.h
@@ -243,6 +243,12 @@ typedef enum {
 	SalOpSDPSimulateRemove /** Will simulate no SDP in the op */
 } SalOpSDPHandling;
 
+#define SAL_STREAM_DESCRIPTION_PORT_TO_BE_DETERMINED 65536
+
+typedef struct SalStreamBundle{
+	bctbx_list_t *mids; /* List of mids corresponding to streams associated in the bundle. The first one is the "tagged" one. */
+} SalStreamBundle;
+
 typedef struct SalStreamDescription{
 	char name[16]; /*unique name of stream, in order to ease offer/answer model algorithm*/
 	SalMediaProto proto;
@@ -264,7 +270,9 @@ typedef struct SalStreamDescription{
 	SalSrtpCryptoAlgo crypto[SAL_CRYPTO_ALGO_MAX];
 	unsigned int crypto_local_tag;
 	int max_rate;
+	bool_t bundle_only;
 	bool_t implicit_rtcp_fb;
+	bool_t pad[2]; /* Use me */
 	OrtpRtcpFbConfiguration rtcp_fb;
 	OrtpRtcpXrConfiguration rtcp_xr;
 	SalCustomSdpAttribute *custom_sdp_attributes;
@@ -272,14 +280,15 @@ typedef struct SalStreamDescription{
 	SalIceRemoteCandidate ice_remote_candidates[SAL_MEDIA_DESCRIPTION_MAX_ICE_REMOTE_CANDIDATES];
 	char ice_ufrag[SAL_MEDIA_DESCRIPTION_MAX_ICE_UFRAG_LEN];
 	char ice_pwd[SAL_MEDIA_DESCRIPTION_MAX_ICE_PWD_LEN];
+	char mid[32]; /* Media line identifier for RTP bundle mode */
+	int mid_rtp_ext_header_id; /* Identifier for the MID field in the RTP extension header */
 	bool_t ice_mismatch;
 	bool_t set_nortpproxy; /*Formely set by ICE to indicate to the proxy that it has nothing to do*/
 	bool_t rtcp_mux;
-	bool_t pad[1];
+	uint8_t haveZrtpHash; /**< flag for zrtp hash presence */
+	uint8_t zrtphash[128];
 	char dtls_fingerprint[256];
 	SalDtlsRole dtls_role;
-	uint8_t zrtphash[128];
-	uint8_t haveZrtpHash; /**< flag for zrtp hash presence */
 	int ttl; /*for multicast -1 to disable*/
 	SalMulticastRole multicast_role;
 } SalStreamDescription;
@@ -313,9 +322,11 @@ typedef struct SalMediaDescription{
 	OrtpRtcpXrConfiguration rtcp_xr;
 	char ice_ufrag[SAL_MEDIA_DESCRIPTION_MAX_ICE_UFRAG_LEN];
 	char ice_pwd[SAL_MEDIA_DESCRIPTION_MAX_ICE_PWD_LEN];
+	bctbx_list_t *bundles; /* list of SalStreamBundle */
 	bool_t ice_lite;
 	bool_t set_nortpproxy;
-	bool_t pad[2];
+	bool_t accept_bundles; /* Set to TRUE if RTP bundles can be accepted during offer answer. This field has no appearance on the SDP.*/
+	bool_t pad[1];
 } SalMediaDescription;
 
 typedef struct SalMessage{
@@ -348,7 +359,12 @@ SalStreamDescription * sal_media_description_find_secure_stream_of_type(SalMedia
 SalStreamDescription * sal_media_description_find_best_stream(SalMediaDescription *md, SalStreamType type);
 void sal_media_description_set_dir(SalMediaDescription *md, SalStreamDir stream_dir);
 int sal_stream_description_equals(const SalStreamDescription *sd1, const SalStreamDescription *sd2);
-bool_t sal_stream_description_active(const SalStreamDescription *sd);
+
+
+/* Enabled means that the stream exists and is accepted as part of the session: the port value is non-zero or the stream has bundle-only attribute.
+ *However, it may be marked with a=inactive, which is unrelated to the return value of this function.*/
+bool_t sal_stream_description_enabled(const SalStreamDescription *sd);
+void sal_stream_description_disable(SalStreamDescription *sd);
 bool_t sal_stream_description_has_avpf(const SalStreamDescription *sd);
 bool_t sal_stream_description_has_implicit_avpf(const SalStreamDescription *sd);
 bool_t sal_stream_description_has_srtp(const SalStreamDescription *sd);
@@ -362,6 +378,18 @@ bool_t sal_media_description_has_zrtp(const SalMediaDescription *md);
 bool_t sal_media_description_has_ipv6(const SalMediaDescription *md);
 int sal_media_description_get_nb_active_streams(const SalMediaDescription *md);
 
+SalStreamBundle * sal_media_description_add_new_bundle(SalMediaDescription *md);
+/* Add stream to the bundle. The SalStreamDescription must be part of the SalMediaDescription in which the SalStreamBundle is added. */
+void sal_stream_bundle_add_stream(SalStreamBundle *bundle, SalStreamDescription *stream, const char *mid);
+void sal_stream_bundle_destroy(SalStreamBundle *bundle);
+SalStreamBundle *sal_stream_bundle_clone(const SalStreamBundle *bundle);
+int sal_stream_bundle_has_mid(const SalStreamBundle *bundle, const char *mid);
+const char *sal_stream_bundle_get_mid_of_transport_owner(const SalStreamBundle *bundle);
+
+int sal_media_description_lookup_mid(const SalMediaDescription *md, const char *mid);
+int sal_media_description_get_index_of_transport_owner(const SalMediaDescription *md, const SalStreamDescription *sd);
+
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/call/call-p.h b/src/call/call-p.h
index c2f52fba1f320d177d2a86bb2245617a11c1251d..93971d5fde3aded2fb0648878d32782f924bdfde 100644
--- a/src/call/call-p.h
+++ b/src/call/call-p.h
@@ -55,6 +55,7 @@ public:
 	unsigned int getAudioStartCount () const;
 	unsigned int getVideoStartCount () const;
 	unsigned int getTextStartCount () const;
+	// don't make new code relying on this method.
 	MediaStream *getMediaStream (LinphoneStreamType type) const;
 	SalCallOp *getOp () const;
 
@@ -96,7 +97,7 @@ private:
 	void onIncomingCallSessionStarted (const std::shared_ptr<CallSession> &session) override;
 	void onIncomingCallSessionTimeoutCheck (const std::shared_ptr<CallSession> &session, int elapsed, bool oneSecondElapsed) override;
 	void onInfoReceived (const std::shared_ptr<CallSession> &session, const LinphoneInfoMessage *im) override;
-	void onNoMediaTimeoutCheck (const std::shared_ptr<CallSession> &session, bool oneSecondElapsed) override;
+	void onLossOfMediaDetected (const std::shared_ptr<CallSession> &session) override;
 	void onEncryptionChanged (const std::shared_ptr<CallSession> &session, bool activated, const std::string &authToken) override;
 	void onCallSessionStateChangedForReporting (const std::shared_ptr<CallSession> &session) override;
 	void onRtcpUpdateForReporting (const std::shared_ptr<CallSession> &session, SalStreamType type) override;
diff --git a/src/call/call.cpp b/src/call/call.cpp
index a5925302e3d648bc49153ff5b7cc11601f4a258f..c23326c81a3cd5b9bdd83a2fe1c3e37da332352f 100644
--- a/src/call/call.cpp
+++ b/src/call/call.cpp
@@ -47,8 +47,10 @@ LinphoneProxyConfig *CallPrivate::getDestProxy () const {
 	return getActiveSession()->getPrivate()->getDestProxy();
 }
 
+/* This a test-only method.*/
 IceSession *CallPrivate::getIceSession () const {
 	return static_pointer_cast<MediaSession>(getActiveSession())->getPrivate()->getIceSession();
+	return nullptr;
 }
 
 unsigned int CallPrivate::getAudioStartCount () const {
@@ -64,7 +66,27 @@ unsigned int CallPrivate::getTextStartCount () const {
 }
 
 MediaStream *CallPrivate::getMediaStream (LinphoneStreamType type) const {
-	return static_pointer_cast<MediaSession>(getActiveSession())->getPrivate()->getMediaStream(type);
+	auto ms = static_pointer_cast<MediaSession>(getActiveSession())->getPrivate();
+	StreamsGroup & sg = ms->getStreamsGroup();
+	MS2Stream *s = nullptr;
+	switch(type){
+		case LinphoneStreamTypeAudio:
+			s = sg.lookupMainStreamInterface<MS2Stream>(SalAudio);
+		break;
+		case LinphoneStreamTypeVideo:
+			s = sg.lookupMainStreamInterface<MS2Stream>(SalVideo);
+		break;
+		case LinphoneStreamTypeText:
+			s = sg.lookupMainStreamInterface<MS2Stream>(SalText);
+		break;
+		default:
+		break;
+	}
+	if (!s){
+		lError() << "CallPrivate::getMediaStream() : no stream with type " << type;
+		return nullptr;
+	}
+	return s->getMediaStream();
 }
 
 SalCallOp * CallPrivate::getOp () const {
@@ -129,7 +151,7 @@ shared_ptr<Call> CallPrivate::startReferredCall (const MediaSessionParams *param
 	if (params)
 		msp = *params;
 	else {
-		msp.initDefault(q->getCore());
+		msp.initDefault(q->getCore(), LinphoneCallOutgoing);
 		msp.enableAudio(q->getCurrentParams()->audioEnabled());
 		msp.enableVideo(q->getCurrentParams()->videoEnabled());
 	}
@@ -155,7 +177,7 @@ void CallPrivate::createPlayer () const {
 // -----------------------------------------------------------------------------
 
 void CallPrivate::initializeMediaStreams () {
-	static_pointer_cast<MediaSession>(getActiveSession())->getPrivate()->initializeStreams();
+	
 }
 
 void CallPrivate::stopMediaStreams () {
@@ -172,13 +194,12 @@ void CallPrivate::startRemoteRing () {
 		return;
 
 	MSSndCard *ringCard = lc->sound_conf.lsd_card ? lc->sound_conf.lsd_card : lc->sound_conf.play_sndcard;
-	int maxRate = static_pointer_cast<MediaSession>(getActiveSession())->getPrivate()->getLocalDesc()->streams[0].max_rate;
-	if (maxRate > 0)
-		ms_snd_card_set_preferred_sample_rate(ringCard, maxRate);
-	// We release sound before playing ringback tone
-	AudioStream *as = reinterpret_cast<AudioStream *>(getMediaStream(LinphoneStreamTypeAudio));
-	if (as)
-		audio_stream_unprepare_sound(as);
+	SalMediaDescription *md = static_pointer_cast<MediaSession>(getActiveSession())->getPrivate()->getLocalDesc();
+	if (md){
+		int maxRate = md->streams[0].max_rate;
+		if (maxRate > 0)
+			ms_snd_card_set_preferred_sample_rate(ringCard, maxRate);
+	}
 	if (lc->sound_conf.remote_ring) {
 		ms_snd_card_set_stream_type(ringCard, MS_SND_CARD_STREAM_VOICE);
 		lc->ringstream = ring_start(lc->factory, lc->sound_conf.remote_ring, 2000, ringCard);
@@ -363,16 +384,8 @@ void CallPrivate::onInfoReceived (const shared_ptr<CallSession> &session, const
 	linphone_call_notify_info_message_received(L_GET_C_BACK_PTR(q), im);
 }
 
-void CallPrivate::onNoMediaTimeoutCheck (const shared_ptr<CallSession> &session, bool oneSecondElapsed) {
-	L_Q();
-	int disconnectTimeout = linphone_core_get_nortp_timeout(q->getCore()->getCCore());
-	bool disconnected = false;
-	AudioStream *as = reinterpret_cast<AudioStream *>(getMediaStream(LinphoneStreamTypeAudio));
-	if (((q->getState() == CallSession::State::StreamsRunning) || (q->getState() == CallSession::State::PausedByRemote))
-		&& oneSecondElapsed && as && (as->ms.state == MSStreamStarted) && (disconnectTimeout > 0))
-		disconnected = !audio_stream_alive(as, disconnectTimeout);
-	if (disconnected)
-		terminateBecauseOfLostMedia();
+void CallPrivate::onLossOfMediaDetected (const shared_ptr<CallSession> &session) {
+	terminateBecauseOfLostMedia();
 }
 
 void CallPrivate::onEncryptionChanged (const shared_ptr<CallSession> &session, bool activated, const string &authToken) {
diff --git a/src/call/call.h b/src/call/call.h
index 19946f99dc641f6cfd3c6ccc5306ee0ac7161bf1..667a1e651ba04d84d0925ae778bba54cbea6d6f3 100644
--- a/src/call/call.h
+++ b/src/call/call.h
@@ -40,6 +40,7 @@ class LINPHONE_PUBLIC Call : public Object, public CoreAccessor {
 	friend class ChatMessagePrivate;
 	friend class CorePrivate;
 	friend class MediaSessionPrivate;
+	friend class Stream;
 
 public:
 	L_OVERRIDE_SHARED_FROM_THIS(Call);
diff --git a/src/conference/params/call-session-params.cpp b/src/conference/params/call-session-params.cpp
index 20b94bee3381968239988de6d99d02839d8c9c2f..4c7a3b786172167c8e5c39fccaf3bdcd9e8038f1 100644
--- a/src/conference/params/call-session-params.cpp
+++ b/src/conference/params/call-session-params.cpp
@@ -87,7 +87,7 @@ CallSessionParams &CallSessionParams::operator= (const CallSessionParams &other)
 
 // -----------------------------------------------------------------------------
 
-void CallSessionParams::initDefault (const std::shared_ptr<Core> &core) {
+void CallSessionParams::initDefault (const std::shared_ptr<Core> &core, LinphoneCallDir dir) {
 	L_D();
 	d->inConference = false;
 	d->privacy = LinphonePrivacyDefault;
diff --git a/src/conference/params/call-session-params.h b/src/conference/params/call-session-params.h
index 23c1855880cd3377db98a1a0196265da3a9e63d9..23da09de094893d8de8277d4db8904ad47e2ebda 100644
--- a/src/conference/params/call-session-params.h
+++ b/src/conference/params/call-session-params.h
@@ -50,7 +50,7 @@ public:
 
 	CallSessionParams &operator= (const CallSessionParams &other);
 
-	virtual void initDefault (const std::shared_ptr<Core> &core);
+	virtual void initDefault (const std::shared_ptr<Core> &core, LinphoneCallDir dir);
 
 	const std::string& getSessionName () const;
 	void setSessionName (const std::string &sessionName);
diff --git a/src/conference/params/media-session-params-p.h b/src/conference/params/media-session-params-p.h
index 603b0b7e791bc6b42bd6d64fc6c31927cd65a676..8fd29cbf043a89dbb77944e8e265377e9b399d5d 100644
--- a/src/conference/params/media-session-params-p.h
+++ b/src/conference/params/media-session-params-p.h
@@ -55,8 +55,12 @@ public:
 	void setDownPtime (int value) { downPtime = value; }
 	int getUpPtime () const { return upPtime; }
 	void setUpPtime (int value) { upPtime = value; }
-	bool getUpdateCallWhenIceCompleted () const { return updateCallWhenIceCompleted; }
-	void setUpdateCallWhenIceCompleted (bool value) { updateCallWhenIceCompleted = value; }
+	bool getUpdateCallWhenIceCompleted () const;
+	void setUpdateCallWhenIceCompleted(bool value){
+		/* apply to both case when set explicitely */
+		updateCallWhenIceCompleted = value;
+		updateCallWhenIceCompletedWithDTLS = value;
+	}
 
 	void setReceivedFps (float value) { receivedFps = value; }
 	void setReceivedVideoDefinition (LinphoneVideoDefinition *value);
@@ -102,6 +106,8 @@ public:
 
 	LinphoneMediaEncryption encryption = LinphoneMediaEncryptionNone;
 	bool mandatoryMediaEncryptionEnabled = false;
+	
+	bool rtpBundle = false;
 
 private:
 	bool _implicitRtcpFbEnabled = false;
@@ -110,6 +116,7 @@ private:
 	int downPtime = 0;
 	int upPtime = 0;
 	bool updateCallWhenIceCompleted = true;
+	bool updateCallWhenIceCompletedWithDTLS = false;
 	SalCustomSdpAttribute *customSdpAttributes = nullptr;
 	SalCustomSdpAttribute *customSdpMediaAttributes[LinphoneStreamTypeUnknown];
 
diff --git a/src/conference/params/media-session-params.cpp b/src/conference/params/media-session-params.cpp
index bdfd07346b9a6d5b2fd2c37dd8cccef312b47e18..4df4dca583b350d744042493148b6dfa8fab3b59 100644
--- a/src/conference/params/media-session-params.cpp
+++ b/src/conference/params/media-session-params.cpp
@@ -71,6 +71,7 @@ void MediaSessionParamsPrivate::clone (const MediaSessionParamsPrivate *src) {
 		if (src->customSdpMediaAttributes[i])
 			customSdpMediaAttributes[i] = sal_custom_sdp_attribute_clone(src->customSdpMediaAttributes[i]);
 	}
+	rtpBundle = src->rtpBundle;
 }
 
 void MediaSessionParamsPrivate::clean () {
@@ -197,6 +198,13 @@ void MediaSessionParamsPrivate::setCustomSdpMediaAttributes (LinphoneStreamType
 		customSdpMediaAttributes[lst] = sal_custom_sdp_attribute_clone(csa);
 }
 
+bool MediaSessionParamsPrivate::getUpdateCallWhenIceCompleted() const{
+	if (encryption == LinphoneMediaEncryptionDTLS){
+		return updateCallWhenIceCompletedWithDTLS;
+	}
+	return updateCallWhenIceCompleted;
+}
+
 // =============================================================================
 
 MediaSessionParams::MediaSessionParams () : CallSessionParams(*new MediaSessionParamsPrivate) {
@@ -225,15 +233,20 @@ MediaSessionParams &MediaSessionParams::operator= (const MediaSessionParams &oth
 
 // -----------------------------------------------------------------------------
 
-void MediaSessionParams::initDefault (const std::shared_ptr<Core> &core) {
+void MediaSessionParams::initDefault (const std::shared_ptr<Core> &core, LinphoneCallDir dir) {
 	L_D();
-	CallSessionParams::initDefault(core);
+	CallSessionParams::initDefault(core, dir);
 	LinphoneCore *cCore = core->getCCore();
 	d->audioEnabled = true;
-	d->videoEnabled = linphone_core_video_enabled(cCore) && cCore->video_policy.automatically_initiate;
-	if (!linphone_core_video_enabled(cCore) && cCore->video_policy.automatically_initiate) {
+	if (dir == LinphoneCallOutgoing){
+		d->videoEnabled = cCore->video_policy.automatically_initiate;
+	}else{
+		d->videoEnabled = cCore->video_policy.automatically_accept;
+	}
+	if (!linphone_core_video_enabled(cCore) && d->videoEnabled) {
 		lError() << "LinphoneCore has video disabled for both capture and display, but video policy is to start the call with video. "
 			"This is a possible mis-use of the API. In this case, video is disabled in default LinphoneCallParams";
+		d->videoEnabled = false;
 	}
 	d->realtimeTextEnabled = !!linphone_core_realtime_text_enabled(cCore);
 	d->realtimeTextKeepaliveInterval = linphone_core_realtime_text_get_keepalive_interval(cCore);
@@ -247,7 +260,16 @@ void MediaSessionParams::initDefault (const std::shared_ptr<Core> &core) {
 	d->audioMulticastEnabled = !!linphone_core_audio_multicast_enabled(cCore);
 	d->videoMulticastEnabled = !!linphone_core_video_multicast_enabled(cCore);
 	d->updateCallWhenIceCompleted = !!lp_config_get_int(linphone_core_get_config(cCore), "sip", "update_call_when_ice_completed", true);
+	/*
+	 * At the time of WebRTC/JSSIP interoperability tests, it was found that the ICE re-INVITE was breaking communication.
+	 * The update_call_when_ice_completed_with_dtls property is hence set to false.
+	 * If this is no longer the case it should be changed to true.
+	 * Otherwise an application may decide to set to true as ICE reINVITE is mandatory per ICE RFC and unless from this WebRTC interoperability standpoint
+	 * there is no problem in having the ICE re-INVITE to be done when SRTP-DTLS is used.
+	 */
+	d->updateCallWhenIceCompletedWithDTLS = linphone_config_get_bool(linphone_core_get_config(cCore), "sip", "update_call_when_ice_completed_with_dtls", false);
 	d->mandatoryMediaEncryptionEnabled = !!linphone_core_is_media_encryption_mandatory(cCore);
+	d->rtpBundle = linphone_core_rtp_bundle_enabled(cCore);
 }
 
 // -----------------------------------------------------------------------------
@@ -265,8 +287,8 @@ bool MediaSessionParams::audioMulticastEnabled () const {
 void MediaSessionParams::enableAudio (bool value) {
 	L_D();
 	d->audioEnabled = value;
-	if (d->audioEnabled && (getAudioDirection() == LinphoneMediaDirectionInactive))
-		setAudioDirection(LinphoneMediaDirectionSendRecv);
+	//if (d->audioEnabled && (getAudioDirection() == LinphoneMediaDirectionInactive))
+	//	setAudioDirection(LinphoneMediaDirectionSendRecv);
 }
 
 void MediaSessionParams::enableAudioMulticast (bool value) {
@@ -309,8 +331,8 @@ void MediaSessionParams::setAudioDirection (LinphoneMediaDirection direction) {
 void MediaSessionParams::enableVideo (bool value) {
 	L_D();
 	d->videoEnabled = value;
-	if (d->videoEnabled && (getVideoDirection() == LinphoneMediaDirectionInactive))
-		setVideoDirection(LinphoneMediaDirectionSendRecv);
+	//if (d->videoEnabled && (getVideoDirection() == LinphoneMediaDirectionInactive))
+	//	setVideoDirection(LinphoneMediaDirectionSendRecv);
 }
 
 void MediaSessionParams::enableVideoMulticast (bool value) {
@@ -529,4 +551,14 @@ const char * MediaSessionParams::getCustomSdpMediaAttribute (LinphoneStreamType
 	return sal_custom_sdp_attribute_find(d->customSdpMediaAttributes[lst], attributeName.c_str());
 }
 
+void MediaSessionParams::enableRtpBundle(bool value){
+	L_D();
+	d->rtpBundle = value;
+}
+
+bool MediaSessionParams::rtpBundleEnabled()const{
+	L_D();
+	return d->rtpBundle;
+}
+
 LINPHONE_END_NAMESPACE
diff --git a/src/conference/params/media-session-params.h b/src/conference/params/media-session-params.h
index 8bb3c2793aabf520c3c0c20d35d7143a6d6a813c..e57cc2a3e75edc0970335b9cfc3a49d37f97e7af 100644
--- a/src/conference/params/media-session-params.h
+++ b/src/conference/params/media-session-params.h
@@ -35,6 +35,10 @@ class MediaSessionParamsPrivate;
 class MediaSessionParams : public CallSessionParams {
 	friend class MediaSession;
 	friend class MediaSessionPrivate;
+	friend class MS2Stream;
+	friend class MS2AudioStream;
+	friend class MS2VideoStream;
+	friend class MS2RTTStream;
 
 public:
 	MediaSessionParams ();
@@ -47,7 +51,7 @@ public:
 
 	MediaSessionParams &operator= (const MediaSessionParams &other);
 
-	void initDefault (const std::shared_ptr<Core> &core) override;
+	void initDefault (const std::shared_ptr<Core> &core, LinphoneCallDir dir) override;
 
 	bool audioEnabled () const;
 	bool audioMulticastEnabled () const;
@@ -109,6 +113,9 @@ public:
 	void addCustomSdpMediaAttribute (LinphoneStreamType lst, const std::string &attributeName, const std::string &attributeValue);
 	void clearCustomSdpMediaAttributes (LinphoneStreamType lst);
 	const char * getCustomSdpMediaAttribute (LinphoneStreamType lst, const std::string &attributeName) const;
+	
+	void enableRtpBundle(bool value);
+	bool rtpBundleEnabled()const;
 
 private:
 	L_DECLARE_PRIVATE(MediaSessionParams);
diff --git a/src/conference/session/audio-stream.cpp b/src/conference/session/audio-stream.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..57205fdeb2bda509eae7928372a5e3cca1c90f47
--- /dev/null
+++ b/src/conference/session/audio-stream.cpp
@@ -0,0 +1,713 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#include "bctoolbox/defs.h"
+
+#include "ms2-streams.h"
+#include "media-session.h"
+#include "media-session-p.h"
+#include "core/core.h"
+#include "c-wrapper/c-wrapper.h"
+#include "call/call.h"
+#include "call/call-p.h"
+#include "conference/participant.h"
+#include "conference/params/media-session-params-p.h"
+#include "nat/ice-service.h"
+
+#include "mediastreamer2/msfileplayer.h"
+#include "mediastreamer2/msvolume.h"
+
+#include "linphone/core.h"
+
+#include <cmath>
+
+using namespace::std;
+
+LINPHONE_BEGIN_NAMESPACE
+
+/*
+ * MS2AudioStream implementation.
+ */
+
+MS2AudioStream::MS2AudioStream(StreamsGroup &sg, const OfferAnswerContext &params) : MS2Stream(sg, params){
+	string bindIp = getBindIp();
+	mStream = audio_stream_new2(getCCore()->factory, bindIp.empty() ? nullptr : bindIp.c_str(), mPortConfig.rtpPort, mPortConfig.rtcpPort);
+
+	/* Initialize zrtp even if we didn't explicitely set it, just in case peer offers it */
+	if (linphone_core_media_encryption_supported(getCCore(), LinphoneMediaEncryptionZRTP)) {
+		LinphoneCallLog *log = getMediaSession().getLog();
+		const LinphoneAddress *peerAddr = linphone_call_log_get_remote_address(log);
+		const LinphoneAddress *selfAddr = linphone_call_log_get_local_address(log);
+		char *peerUri = ms_strdup_printf("%s:%s@%s"	, linphone_address_get_scheme(peerAddr)
+													, linphone_address_get_username(peerAddr)
+													, linphone_address_get_domain(peerAddr));
+		char *selfUri = ms_strdup_printf("%s:%s@%s"	, linphone_address_get_scheme(selfAddr)
+													, linphone_address_get_username(selfAddr)
+													, linphone_address_get_domain(selfAddr));
+
+		MSZrtpParams zrtpParams;
+		zrtpCacheAccess zrtpCacheInfo = linphone_core_get_zrtp_cache_access(getCCore());
+
+		memset(&zrtpParams, 0, sizeof(MSZrtpParams));
+		/* media encryption of current params will be set later when zrtp is activated */
+		zrtpParams.zidCacheDB = zrtpCacheInfo.db;
+		zrtpParams.zidCacheDBMutex = zrtpCacheInfo.dbMutex;
+		zrtpParams.peerUri = peerUri;
+		zrtpParams.selfUri = selfUri;
+		/* Get key lifespan from config file, default is 0:forever valid */
+		zrtpParams.limeKeyTimeSpan = bctbx_time_string_to_sec(lp_config_get_string(linphone_core_get_config(getCCore()), "sip", "lime_key_validity", "0"));
+		setZrtpCryptoTypesParameters(&zrtpParams, params.remoteStreamDescription ? params.remoteStreamDescription->haveZrtpHash : false);
+		audio_stream_enable_zrtp(mStream, &zrtpParams);
+		if (peerUri)
+			ms_free(peerUri);
+		if (selfUri)
+			ms_free(selfUri);
+	}
+	initializeSessions((MediaStream*)mStream);
+}
+
+void MS2AudioStream::setZrtpCryptoTypesParameters(MSZrtpParams *params, bool haveRemoteZrtpHash) {
+	const MSCryptoSuite *srtpSuites = linphone_core_get_srtp_crypto_suites(getCCore());
+	if (srtpSuites) {
+		for(int i = 0; (srtpSuites[i] != MS_CRYPTO_SUITE_INVALID) && (i < SAL_CRYPTO_ALGO_MAX) && (i < MS_MAX_ZRTP_CRYPTO_TYPES); i++) {
+			switch (srtpSuites[i]) {
+				case MS_AES_128_SHA1_32:
+					params->ciphers[params->ciphersCount++] = MS_ZRTP_CIPHER_AES1;
+					params->authTags[params->authTagsCount++] = MS_ZRTP_AUTHTAG_HS32;
+					break;
+				case MS_AES_128_NO_AUTH:
+					params->ciphers[params->ciphersCount++] = MS_ZRTP_CIPHER_AES1;
+					break;
+				case MS_NO_CIPHER_SHA1_80:
+					params->authTags[params->authTagsCount++] = MS_ZRTP_AUTHTAG_HS80;
+					break;
+				case MS_AES_128_SHA1_80:
+					params->ciphers[params->ciphersCount++] = MS_ZRTP_CIPHER_AES1;
+					params->authTags[params->authTagsCount++] = MS_ZRTP_AUTHTAG_HS80;
+					break;
+				case MS_AES_CM_256_SHA1_80:
+					lWarning() << "Deprecated crypto suite MS_AES_CM_256_SHA1_80, use MS_AES_256_SHA1_80 instead";
+					BCTBX_NO_BREAK;
+				case MS_AES_256_SHA1_80:
+					params->ciphers[params->ciphersCount++] = MS_ZRTP_CIPHER_AES3;
+					params->authTags[params->authTagsCount++] = MS_ZRTP_AUTHTAG_HS80;
+					break;
+				case MS_AES_256_SHA1_32:
+					params->ciphers[params->ciphersCount++] = MS_ZRTP_CIPHER_AES3;
+					params->authTags[params->authTagsCount++] = MS_ZRTP_AUTHTAG_HS32;
+					break;
+				case MS_CRYPTO_SUITE_INVALID:
+					break;
+			}
+		}
+	}
+
+	/* linphone_core_get_srtp_crypto_suites is used to determine sensible defaults; here each can be overridden */
+	MsZrtpCryptoTypesCount ciphersCount = linphone_core_get_zrtp_cipher_suites(getCCore(), params->ciphers); /* if not present in config file, params->ciphers is not modified */
+	if (ciphersCount != 0) /* Use zrtp_cipher_suites config only when present, keep config from srtp_crypto_suite otherwise */
+		params->ciphersCount = ciphersCount;
+	params->hashesCount = linphone_core_get_zrtp_hash_suites(getCCore(), params->hashes);
+	MsZrtpCryptoTypesCount authTagsCount = linphone_core_get_zrtp_auth_suites(getCCore(), params->authTags); /* If not present in config file, params->authTags is not modified */
+	if (authTagsCount != 0)
+		params->authTagsCount = authTagsCount; /* Use zrtp_auth_suites config only when present, keep config from srtp_crypto_suite otherwise */
+	params->sasTypesCount = linphone_core_get_zrtp_sas_suites(getCCore(), params->sasTypes);
+	params->keyAgreementsCount = linphone_core_get_zrtp_key_agreement_suites(getCCore(), params->keyAgreements);
+	
+	params->autoStart =  (getMediaSessionPrivate().getParams()->getMediaEncryption() != LinphoneMediaEncryptionZRTP) && (haveRemoteZrtpHash == false) ;
+}
+
+void MS2AudioStream::configureAudioStream(){
+	MSSndCard *playcard = getCCore()->sound_conf.lsd_card ? getCCore()->sound_conf.lsd_card : getCCore()->sound_conf.play_sndcard;
+	if (playcard) {
+		// Set the stream type immediately, as on iOS AudioUnit is instanciated very early because it is 
+		// otherwise too slow to start.
+		ms_snd_card_set_stream_type(playcard, MS_SND_CARD_STREAM_VOICE);
+	}
+	
+	if (linphone_core_echo_limiter_enabled(getCCore())) {
+		string type = lp_config_get_string(linphone_core_get_config(getCCore()), "sound", "el_type", "mic");
+		if (type == "mic")
+			audio_stream_enable_echo_limiter(mStream, ELControlMic);
+		else if (type == "full")
+			audio_stream_enable_echo_limiter(mStream, ELControlFull);
+	}
+
+	// Equalizer location in the graph: 'mic' = in input graph, otherwise in output graph.
+	// Any other value than mic will default to output graph for compatibility.
+	string location = lp_config_get_string(linphone_core_get_config(getCCore()), "sound", "eq_location", "hp");
+	mStream->eq_loc = (location == "mic") ? MSEqualizerMic : MSEqualizerHP;
+	lInfo() << "Equalizer location: " << location;
+
+	audio_stream_enable_gain_control(mStream, true);
+	if (linphone_core_echo_cancellation_enabled(getCCore())) {
+		int len = lp_config_get_int(linphone_core_get_config(getCCore()), "sound", "ec_tail_len", 0);
+		int delay = lp_config_get_int(linphone_core_get_config(getCCore()), "sound", "ec_delay", 0);
+		int framesize = lp_config_get_int(linphone_core_get_config(getCCore()), "sound", "ec_framesize", 0);
+		audio_stream_set_echo_canceller_params(mStream, len, delay, framesize);
+		if (mStream->ec) {
+			char *statestr=static_cast<char *>(ms_malloc0(ecStateMaxLen));
+			if (lp_config_relative_file_exists(linphone_core_get_config(getCCore()), ecStateStore)
+				&& (lp_config_read_relative_file(linphone_core_get_config(getCCore()), ecStateStore, statestr, ecStateMaxLen) == 0)) {
+				ms_filter_call_method(mStream->ec, MS_ECHO_CANCELLER_SET_STATE_STRING, statestr);
+			}
+			ms_free(statestr);
+		}
+	}
+	audio_stream_enable_automatic_gain_control(mStream, linphone_core_agc_enabled(getCCore()));
+	bool_t enabled = !!lp_config_get_int(linphone_core_get_config(getCCore()), "sound", "noisegate", 0);
+	audio_stream_enable_noise_gate(mStream, enabled);
+	audio_stream_set_features(mStream, linphone_core_get_audio_features(getCCore()));
+}
+
+bool MS2AudioStream::prepare(){
+	MSSndCard *playcard = getCCore()->sound_conf.lsd_card ? getCCore()->sound_conf.lsd_card : getCCore()->sound_conf.play_sndcard;
+	if (playcard) {
+		// Set the stream type immediately, as on iOS AudioUnit is instanciated very early because it is 
+		// otherwise too slow to start.
+		ms_snd_card_set_stream_type(playcard, MS_SND_CARD_STREAM_VOICE);
+	}
+	
+	if (!getCCore()->use_files){
+		audio_stream_prepare_sound(mStream, getCCore()->sound_conf.play_sndcard, getCCore()->sound_conf.capt_sndcard);
+	}else if (getIceService().isActive()){
+		audio_stream_prepare_sound(mStream, nullptr, nullptr);
+	}
+	MS2Stream::prepare();
+	return false;
+}
+
+void MS2AudioStream::sessionConfirmed(const OfferAnswerContext &ctx){
+	if (mStartZrtpLater){
+		lInfo() << "Starting zrtp late";
+		startZrtpPrimaryChannel(ctx);
+		mStartZrtpLater = false;
+	}
+}
+
+void MS2AudioStream::finishPrepare(){
+	MS2Stream::finishPrepare();
+	audio_stream_unprepare_sound(mStream);
+}
+
+MediaStream *MS2AudioStream::getMediaStream()const{
+	return &mStream->ms;
+}
+
+void MS2AudioStream::setupMediaLossCheck(){
+	int disconnectTimeout = linphone_core_get_nortp_timeout(getCCore());
+	mMediaLostCheckTimer = getCore().createTimer( [this, disconnectTimeout]() -> bool{
+			if (!audio_stream_alive(mStream, disconnectTimeout)){
+				CallSessionListener *listener = getMediaSessionPrivate().getCallSessionListener();
+				listener->onLossOfMediaDetected(getMediaSession().getSharedFromThis());
+			}
+			return true;
+		}, 1000, "Audio stream alive check");
+}
+
+void MS2AudioStream::render(const OfferAnswerContext &params, CallSession::State targetState){
+	const SalStreamDescription *stream = params.resultStreamDescription;
+	CallSessionListener *listener = getMediaSessionPrivate().getCallSessionListener();
+	
+	bool basicChangesHandled = handleBasicChanges(params, targetState);
+	
+	if (basicChangesHandled) {
+		if (getState() == Running) {
+			bool muted = mMuted;
+			MS2Stream::render(params, targetState); // MS2Stream::render() may decide to unmute.
+			if (muted && !mMuted) {
+				lInfo() << "Early media finished, unmuting audio input...";
+				enableMic(micEnabled());
+			}
+		}
+		return;
+	}
+	
+	int usedPt = -1;
+	string onHoldFile = "";
+	RtpProfile *audioProfile = makeProfile(params.resultMediaDescription, stream, &usedPt);
+	if (usedPt == -1){
+		lError() << "No payload types configured for this stream !";
+		stop();
+		return;
+	}
+
+	bool ok = true;
+	if (isMain()){
+		getMediaSessionPrivate().getCurrentParams()->getPrivate()->setUsedAudioCodec(rtp_profile_get_payload(audioProfile, usedPt));
+	}
+	MSSndCard *playcard = getCCore()->sound_conf.lsd_card ? getCCore()->sound_conf.lsd_card : getCCore()->sound_conf.play_sndcard;
+	if (!playcard)
+		lWarning() << "No card defined for playback!";
+	MSSndCard *captcard = getCCore()->sound_conf.capt_sndcard;
+	if (!captcard)
+		lWarning() << "No card defined for capture!";
+	string playfile = L_C_TO_STRING(getCCore()->play_file);
+	string recfile = L_C_TO_STRING(getCCore()->rec_file);
+	/* Don't use file or soundcard capture when placed in recv-only mode */
+	if ((stream->rtp_port == 0) || (stream->dir == SalStreamRecvOnly) || (stream->multicast_role == SalMulticastReceiver)) {
+		captcard = nullptr;
+		playfile = "";
+	}
+
+	if (targetState == CallSession::State::Paused) {
+		// In paused state, we never use soundcard
+		playcard = captcard = nullptr;
+		recfile = "";
+		// And we will eventually play "playfile" if set by the user
+	}
+	if (listener && listener->isPlayingRingbackTone(getMediaSession().getSharedFromThis())) {
+		captcard = nullptr;
+		playfile = ""; /* It is setup later */
+		if (lp_config_get_int(linphone_core_get_config(getCCore()), "sound", "send_ringback_without_playback", 0) == 1) {
+			playcard = nullptr;
+			recfile = "";
+		}
+	}
+	// If playfile are supplied don't use soundcards
+	bool useRtpIo = !!lp_config_get_int(linphone_core_get_config(getCCore()), "sound", "rtp_io", false);
+	bool useRtpIoEnableLocalOutput = !!lp_config_get_int(linphone_core_get_config(getCCore()), "sound", "rtp_io_enable_local_output", false);
+	if (getCCore()->use_files || (useRtpIo && !useRtpIoEnableLocalOutput)) {
+		captcard = playcard = nullptr;
+	}
+	if (getMediaSessionPrivate().getParams()->getPrivate()->getInConference()) {
+		// First create the graph without soundcard resources
+		captcard = playcard = nullptr;
+	}
+	if (listener && !listener->areSoundResourcesAvailable(getMediaSession().getSharedFromThis())) {
+		lInfo() << "Sound resources are used by another CallSession, not using soundcard";
+		captcard = playcard = nullptr;
+	}
+
+	if (playcard) {
+		ms_snd_card_set_stream_type(playcard, MS_SND_CARD_STREAM_VOICE);
+	}
+	configureAudioStream();
+	bool useEc = captcard && linphone_core_echo_cancellation_enabled(getCCore());
+	audio_stream_enable_echo_canceller(mStream, useEc);
+	if (playcard && (stream->max_rate > 0))
+		ms_snd_card_set_preferred_sample_rate(playcard, stream->max_rate);
+	if (captcard && (stream->max_rate > 0))
+		ms_snd_card_set_preferred_sample_rate(captcard, stream->max_rate);
+	
+	if (!getMediaSessionPrivate().getParams()->getPrivate()->getInConference() && !getMediaSessionPrivate().getParams()->getRecordFilePath().empty()) {
+		audio_stream_mixed_record_open(mStream, getMediaSessionPrivate().getParams()->getRecordFilePath().c_str());
+		getMediaSessionPrivate().getCurrentParams()->setRecordFilePath(getMediaSessionPrivate().getParams()->getRecordFilePath());
+	}
+
+	MS2Stream::render(params, targetState);
+	RtpAddressInfo dest;
+	getRtpDestination(params, &dest);
+	/* Now start the stream */
+	MSMediaStreamIO io = MS_MEDIA_STREAM_IO_INITIALIZER;
+	if (useRtpIo) {
+		if (useRtpIoEnableLocalOutput) {
+			io.input.type = MSResourceRtp;
+			io.input.session = createRtpIoSession();
+			if (playcard) {
+				io.output.type = MSResourceSoundcard;
+				io.output.soundcard = playcard;
+			} else {
+				io.output.type = MSResourceFile;
+				io.output.file = recfile.empty() ? nullptr : recfile.c_str();
+			}
+		} else {
+			io.input.type = io.output.type = MSResourceRtp;
+			io.input.session = io.output.session = createRtpIoSession();
+		}
+		if (!io.input.session)
+			ok = false;
+	} else {
+		if (playcard) {
+			io.output.type = MSResourceSoundcard;
+			io.output.soundcard = playcard;
+		} else {
+			io.output.type = MSResourceFile;
+			io.output.file = recfile.empty() ? nullptr : recfile.c_str();
+		}
+		if (captcard) {
+			io.input.type = MSResourceSoundcard;
+			io.input.soundcard = captcard;
+		} else {
+			io.input.type = MSResourceFile;
+			onHoldFile = playfile;
+			io.input.file = nullptr; /* We prefer to use the remote_play api, that allows to play multimedia files */
+		}
+	}
+	if (ok) {
+		VideoStream *vs = getPeerVideoStream();
+		if (vs) audio_stream_link_video(mStream, vs);
+		mCurrentCaptureCard = ms_media_resource_get_soundcard(&io.input);
+		mCurrentPlaybackCard = ms_media_resource_get_soundcard(&io.output);
+
+		int err = audio_stream_start_from_io(mStream, audioProfile, dest.rtpAddr.c_str(), dest.rtpPort,
+			dest.rtcpAddr.c_str(), dest.rtcpPort, usedPt, &io);
+		if (err == 0)
+			postConfigureAudioStream((mMuted || mMicMuted) && (listener && !listener->isPlayingRingbackTone(getMediaSession().getSharedFromThis())));
+		mStartCount++;
+	}
+	
+	if ((targetState == CallSession::State::Paused) && !captcard && !playfile.empty()) {
+		int pauseTime = 500;
+		ms_filter_call_method(mStream->soundread, MS_FILE_PLAYER_LOOP, &pauseTime);
+	}
+	if (listener && listener->isPlayingRingbackTone(getMediaSession().getSharedFromThis()))
+		setupRingbackPlayer();
+	if (getMediaSessionPrivate().getParams()->getPrivate()->getInConference() && listener) {
+		// Transform the graph to connect it to the conference filter
+		bool mute = (stream->dir == SalStreamRecvOnly);
+		listener->onCallSessionConferenceStreamStarting(getMediaSession().getSharedFromThis(), mute);
+	}
+	getMediaSessionPrivate().getCurrentParams()->getPrivate()->setInConference(getMediaSessionPrivate().getParams()->getPrivate()->getInConference());
+	getMediaSessionPrivate().getCurrentParams()->enableLowBandwidth(getMediaSessionPrivate().getParams()->lowBandwidthEnabled());
+	
+	// Start ZRTP engine if needed : set here or remote have a zrtp-hash attribute
+	if (linphone_core_media_encryption_supported(getCCore(), LinphoneMediaEncryptionZRTP) && isMain()) {
+		getMediaSessionPrivate().performMutualAuthentication();
+		LinphoneMediaEncryption requestedMediaEncryption = getMediaSessionPrivate().getParams()->getMediaEncryption();
+		//Start zrtp if remote has offered it or if local is configured for zrtp and is the offerrer. If not, defered when ACK is received
+		if ((requestedMediaEncryption == LinphoneMediaEncryptionZRTP && params.localIsOfferer)
+			|| (params.remoteStreamDescription->haveZrtpHash == 1)) {
+			startZrtpPrimaryChannel(params);
+		}else if (requestedMediaEncryption == LinphoneMediaEncryptionZRTP && !params.localIsOfferer){
+			mStartZrtpLater = true;
+		}
+	}
+	
+	getGroup().addPostRenderHook([this, onHoldFile] {
+		/* The on-hold file is to be played once both audio and video are ready */
+		if (!onHoldFile.empty() && !getMediaSessionPrivate().getParams()->getPrivate()->getInConference()) {
+			MSFilter *player = audio_stream_open_remote_play(mStream, onHoldFile.c_str());
+			if (player) {
+				int pauseTime = 500;
+				ms_filter_call_method(player, MS_PLAYER_SET_LOOP, &pauseTime);
+				ms_filter_call_method_noarg(player, MS_PLAYER_START);
+			}
+		}
+	});
+	
+	if (targetState == CallSession::State::StreamsRunning){
+		setupMediaLossCheck();
+	}
+	
+	return;
+}
+
+void MS2AudioStream::stop(){
+	if (mMediaLostCheckTimer) {
+		getCore().destroyTimer(mMediaLostCheckTimer);
+		mMediaLostCheckTimer = nullptr;
+	}
+	MS2Stream::stop();
+	if (mStream->ec) {
+		char *stateStr = nullptr;
+		ms_filter_call_method(mStream->ec, MS_ECHO_CANCELLER_GET_STATE_STRING, &stateStr);
+		if (stateStr) {
+			lInfo() << "Writing echo canceler state, " << (int)strlen(stateStr) << " bytes";
+			lp_config_write_relative_file(linphone_core_get_config(getCCore()), ecStateStore, stateStr);
+		}
+	}
+	VideoStream *vs = getPeerVideoStream();
+	if (vs) audio_stream_unlink_video(mStream, vs);
+	
+	audio_stream_stop(mStream);
+	/* In mediastreamer2, stop actually stops and destroys. We immediately need to recreate the stream object for later use, keeping the 
+	 * sessions (for RTP, SRTP, ZRTP etc) that were setup at the beginning. */
+	mStream = audio_stream_new_with_sessions(getCCore()->factory, &mSessions);
+	getMediaSessionPrivate().getCurrentParams()->getPrivate()->setUsedAudioCodec(nullptr);
+	
+	
+	mCurrentCaptureCard = nullptr;
+	mCurrentPlaybackCard = nullptr;
+}
+
+//To give a chance for auxilary secret to be used, primary channel (I.E audio) should be started either on 200ok if ZRTP is signaled by a zrtp-hash or when ACK is received in case calling side does not have zrtp-hash.
+void MS2AudioStream::startZrtpPrimaryChannel(const OfferAnswerContext &params) {
+	const SalStreamDescription *remote = params.remoteStreamDescription;
+	audio_stream_start_zrtp(mStream);
+	if (remote->haveZrtpHash == 1) {
+		int retval = ms_zrtp_setPeerHelloHash(mSessions.zrtp_context, (uint8_t *)remote->zrtphash, strlen((const char *)(remote->zrtphash)));
+		if (retval != 0)
+			lError() << "ZRTP hash mismatch 0x" << hex << retval;
+	}
+}
+
+void MS2AudioStream::forceSpeakerMuted (bool muted) {
+	if (muted)
+		audio_stream_set_spk_gain(mStream, 0);
+	else
+		audio_stream_set_spk_gain_db(mStream, getCCore()->sound_conf.soft_play_lev);
+}
+
+void MS2AudioStream::setRoute(LinphoneAudioRoute route){
+	audio_stream_set_audio_route(mStream, (MSAudioRoute)route);
+}
+
+void MS2AudioStream::parameterizeEqualizer(AudioStream *as, LinphoneCore *lc) {
+	LinphoneConfig *config = linphone_core_get_config(lc);
+	const char *eqActive = lp_config_get_string(config, "sound", "eq_active", nullptr);
+	if (eqActive)
+		lWarning() << "'eq_active' linphonerc parameter has no effect anymore. Please use 'mic_eq_active' or 'spk_eq_active' instead";
+	const char *eqGains = lp_config_get_string(config, "sound", "eq_gains", nullptr);
+	if(eqGains)
+		lWarning() << "'eq_gains' linphonerc parameter has no effect anymore. Please use 'mic_eq_gains' or 'spk_eq_gains' instead";
+	if (as->mic_equalizer) {
+		MSFilter *f = as->mic_equalizer;
+		bool enabled = !!lp_config_get_int(config, "sound", "mic_eq_active", 0);
+		ms_filter_call_method(f, MS_EQUALIZER_SET_ACTIVE, &enabled);
+		const char *gains = lp_config_get_string(config, "sound", "mic_eq_gains", nullptr);
+		if (enabled && gains) {
+			bctbx_list_t *gainsList = ms_parse_equalizer_string(gains);
+			for (bctbx_list_t *it = gainsList; it; it = bctbx_list_next(it)) {
+				MSEqualizerGain *g = reinterpret_cast<MSEqualizerGain *>(bctbx_list_get_data(it));
+				lInfo() << "Read microphone equalizer gains: " << g->frequency << "(~" << g->width << ") --> " << g->gain;
+				ms_filter_call_method(f, MS_EQUALIZER_SET_GAIN, g);
+			}
+			if (gainsList)
+				bctbx_list_free_with_data(gainsList, ms_free);
+		}
+	}
+	if (as->spk_equalizer) {
+		MSFilter *f = as->spk_equalizer;
+		bool enabled = !!lp_config_get_int(config, "sound", "spk_eq_active", 0);
+		ms_filter_call_method(f, MS_EQUALIZER_SET_ACTIVE, &enabled);
+		const char *gains = lp_config_get_string(config, "sound", "spk_eq_gains", nullptr);
+		if (enabled && gains) {
+			bctbx_list_t *gainsList = ms_parse_equalizer_string(gains);
+			for (bctbx_list_t *it = gainsList; it; it = bctbx_list_next(it)) {
+				MSEqualizerGain *g = reinterpret_cast<MSEqualizerGain *>(bctbx_list_get_data(it));
+				lInfo() << "Read speaker equalizer gains: " << g->frequency << "(~" << g->width << ") --> " << g->gain;
+				ms_filter_call_method(f, MS_EQUALIZER_SET_GAIN, g);
+			}
+			if (gainsList)
+				bctbx_list_free_with_data(gainsList, ms_free);
+		}
+	}
+}
+
+void MS2AudioStream::postConfigureAudioStream(AudioStream *as, LinphoneCore *lc, bool muted){
+	float micGain = lc->sound_conf.soft_mic_lev;
+	if (muted)
+		audio_stream_set_mic_gain(as, 0);
+	else
+		audio_stream_set_mic_gain_db(as, micGain);
+	float recvGain = lc->sound_conf.soft_play_lev;
+	if (static_cast<int>(recvGain)){
+		if (as->volrecv)
+			ms_filter_call_method(as->volrecv, MS_VOLUME_SET_DB_GAIN, &recvGain);
+		else
+			lWarning() << "Could not apply playback gain: gain control wasn't activated";
+	}
+	LinphoneConfig *config = linphone_core_get_config(lc);
+	float ngThres = lp_config_get_float(config, "sound", "ng_thres", 0.05f);
+	float ngFloorGain = lp_config_get_float(config, "sound", "ng_floorgain", 0);
+	if (as->volsend) {
+		int dcRemoval = lp_config_get_int(config, "sound", "dc_removal", 0);
+		ms_filter_call_method(as->volsend, MS_VOLUME_REMOVE_DC, &dcRemoval);
+		float speed = lp_config_get_float(config, "sound", "el_speed", -1);
+		float thres = lp_config_get_float(config, "sound", "el_thres", -1);
+		float force = lp_config_get_float(config, "sound", "el_force", -1);
+		int sustain = lp_config_get_int(config, "sound", "el_sustain", -1);
+		float transmitThres = lp_config_get_float(config, "sound", "el_transmit_thres", -1);
+		if (static_cast<int>(speed) == -1)
+			speed = 0.03f;
+		if (static_cast<int>(force) == -1)
+			force = 25;
+		MSFilter *f = as->volsend;
+		ms_filter_call_method(f, MS_VOLUME_SET_EA_SPEED, &speed);
+		ms_filter_call_method(f, MS_VOLUME_SET_EA_FORCE, &force);
+		if (static_cast<int>(thres) != -1)
+			ms_filter_call_method(f, MS_VOLUME_SET_EA_THRESHOLD, &thres);
+		if (static_cast<int>(sustain) != -1)
+			ms_filter_call_method(f, MS_VOLUME_SET_EA_SUSTAIN, &sustain);
+		if (static_cast<int>(transmitThres) != -1)
+			ms_filter_call_method(f, MS_VOLUME_SET_EA_TRANSMIT_THRESHOLD, &transmitThres);
+		ms_filter_call_method(f, MS_VOLUME_SET_NOISE_GATE_THRESHOLD, &ngThres);
+		ms_filter_call_method(f, MS_VOLUME_SET_NOISE_GATE_FLOORGAIN, &ngFloorGain);
+	}
+	if (as->volrecv) {
+		/* Parameters for a limited noise-gate effect, using echo limiter threshold */
+		float floorGain = (float)(1 / pow(10, micGain / 10));
+		int spkAgc = lp_config_get_int(config, "sound", "speaker_agc_enabled", 0);
+		MSFilter *f = as->volrecv;
+		ms_filter_call_method(f, MS_VOLUME_ENABLE_AGC, &spkAgc);
+		ms_filter_call_method(f, MS_VOLUME_SET_NOISE_GATE_THRESHOLD, &ngThres);
+		ms_filter_call_method(f, MS_VOLUME_SET_NOISE_GATE_FLOORGAIN, &floorGain);
+	}
+	parameterizeEqualizer(as, lc);
+}
+
+void MS2AudioStream::postConfigureAudioStream(bool muted) {
+	postConfigureAudioStream(mStream, getCCore(), muted);
+	forceSpeakerMuted(mSpeakerMuted);
+	if (linphone_core_dtmf_received_has_listener(getCCore()))
+		audio_stream_play_received_dtmfs(mStream, false);
+	if (mRecordActive)
+		startRecording();
+}
+
+void MS2AudioStream::setupRingbackPlayer () {
+	int pauseTime = 3000;
+	audio_stream_play(mStream, getCCore()->sound_conf.ringback_tone);
+	ms_filter_call_method(mStream->soundread, MS_FILE_PLAYER_LOOP, &pauseTime);
+}
+
+void MS2AudioStream::telephoneEventReceived (int event) {
+	static char dtmfTab[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#', 'A', 'B', 'C', 'D' };
+	if ((event < 0) || (event > 15)) {
+		lWarning() << "Bad dtmf value " << event;
+		return;
+	}
+	getMediaSessionPrivate().dtmfReceived(dtmfTab[event]);
+}
+
+void MS2AudioStream::handleEvent(const OrtpEvent *ev){
+	OrtpEventType evt = ortp_event_get_type(ev);
+	OrtpEventData *evd = ortp_event_get_data(const_cast<OrtpEvent*>(ev));
+	switch (evt){
+		case ORTP_EVENT_ZRTP_ENCRYPTION_CHANGED:
+			if (isMain()) getGroup().zrtpStarted(this);
+		break;
+		case ORTP_EVENT_ZRTP_SAS_READY:
+			getGroup().authTokenReady(evd->info.zrtp_info.sas, !!evd->info.zrtp_info.verified);
+		break;
+		case ORTP_EVENT_TELEPHONE_EVENT:
+			telephoneEventReceived(evd->info.telephone_event);
+		break;
+	}
+}
+
+void MS2AudioStream::enableMic(bool value){
+	mMicMuted = !value;
+
+	if (mMicMuted)
+		audio_stream_set_mic_gain(mStream, 0);
+	else
+		audio_stream_set_mic_gain_db(mStream, getCCore()->sound_conf.soft_mic_lev);
+}
+
+bool MS2AudioStream::micEnabled()const{
+	return !mMicMuted;
+}
+
+void MS2AudioStream::enableSpeaker(bool value){
+	mSpeakerMuted = !value;
+	forceSpeakerMuted(mSpeakerMuted);
+}
+
+bool MS2AudioStream::speakerEnabled()const{
+	return !mSpeakerMuted;
+}
+
+void MS2AudioStream::sendDtmf(int dtmf){
+	audio_stream_send_dtmf(mStream, (char)dtmf);
+}
+
+void MS2AudioStream::startRecording(){
+	if (getMediaSessionPrivate().getParams()->getRecordFilePath().empty()) {
+		lError() << "MS2AudioStream::startRecording(): no output file specified. Use MediaSessionParams::setRecordFilePath()";
+		return;
+	}
+	if (getMediaSessionPrivate().getParams()->getPrivate()->getInConference()){
+		lWarning() << "MS2AudioStream::startRecording(): not supported in conference.";
+		return;
+	}
+	if (media_stream_get_state(&mStream->ms) == MSStreamStarted) audio_stream_mixed_record_start(mStream);
+	mRecordActive = true;
+}
+
+void MS2AudioStream::stopRecording(){
+	if (mRecordActive)
+		audio_stream_mixed_record_stop(mStream);
+	mRecordActive = false;
+}
+
+float MS2AudioStream::getPlayVolume(){
+	if (mStream->volrecv) {
+		float vol = 0;
+		ms_filter_call_method(mStream->volrecv, MS_VOLUME_GET, &vol);
+		return vol;
+	}
+	return LINPHONE_VOLUME_DB_LOWEST;
+}
+
+float MS2AudioStream::getRecordVolume(){
+	if (mStream->volsend && !mMicMuted) {
+		float vol = 0;
+		ms_filter_call_method(mStream->volsend, MS_VOLUME_GET, &vol);
+		return vol;
+	}
+	return LINPHONE_VOLUME_DB_LOWEST;
+}
+
+float MS2AudioStream::getMicGain(){
+	return audio_stream_get_sound_card_input_gain(mStream);
+}
+
+void MS2AudioStream::setMicGain(float gain){
+	audio_stream_set_sound_card_input_gain(mStream, gain);
+}
+
+float MS2AudioStream::getSpeakerGain(){
+	return audio_stream_get_sound_card_output_gain(mStream);
+}
+
+void MS2AudioStream::setSpeakerGain(float gain){
+	audio_stream_set_sound_card_output_gain(mStream, gain);
+}
+
+VideoStream *MS2AudioStream::getPeerVideoStream(){
+#ifdef VIDEO_ENABLED
+	MS2VideoStream *vs = getGroup().lookupMainStreamInterface<MS2VideoStream>(SalVideo);
+	return vs ? (VideoStream*)vs->getMediaStream() : nullptr;
+#else
+	return nullptr;
+#endif
+}
+
+void MS2AudioStream::enableEchoCancellation(bool value){
+	if (mStream->ec) {
+		bool bypassMode = !value;
+		ms_filter_call_method(mStream->ec, MS_ECHO_CANCELLER_SET_BYPASS_MODE, &bypassMode);
+	}
+	
+}
+
+bool MS2AudioStream::echoCancellationEnabled()const{
+	if (!mStream->ec)
+		return !!linphone_core_echo_cancellation_enabled(getCCore());
+
+	bool_t val;
+	ms_filter_call_method(mStream->ec, MS_ECHO_CANCELLER_GET_BYPASS_MODE, &val);
+	return !val;
+}
+
+void MS2AudioStream::finish(){
+	if (mStream){
+		audio_stream_stop(mStream);
+		mStream = nullptr;
+	}
+	MS2Stream::finish();
+}
+
+MS2AudioStream::~MS2AudioStream(){
+	if (mStream)
+		audio_stream_stop(mStream);
+}
+
+
+LINPHONE_END_NAMESPACE
diff --git a/src/conference/session/call-session-listener.h b/src/conference/session/call-session-listener.h
index a1233e800a067ac601ec1110bf9283a78594a703..947d14c934e097a05dab5a74b88f2ce645b639c1 100644
--- a/src/conference/session/call-session-listener.h
+++ b/src/conference/session/call-session-listener.h
@@ -53,7 +53,7 @@ public:
 	virtual void onIncomingCallSessionStarted (const std::shared_ptr<CallSession> &session) {}
 	virtual void onIncomingCallSessionTimeoutCheck (const std::shared_ptr<CallSession> &session, int elapsed, bool oneSecondElapsed) {}
 	virtual void onInfoReceived (const std::shared_ptr<CallSession> &session, const LinphoneInfoMessage *im) {}
-	virtual void onNoMediaTimeoutCheck (const std::shared_ptr<CallSession> &session, bool oneSecondElapsed) {}
+	virtual void onLossOfMediaDetected (const std::shared_ptr<CallSession> &session) {}
 	virtual void onTmmbrReceived (const std::shared_ptr<CallSession> &session, int streamIndex, int tmmbr) {}
 	virtual void onSnapshotTaken(const std::shared_ptr<CallSession> &session, const char *file_path) {}
 
diff --git a/src/conference/session/call-session-p.h b/src/conference/session/call-session-p.h
index 4e7b1704bb5c7ce2c1544ebda7cdbb0b424d6a26..a052c77bcf3e46f36ba90f7b5a9731f7365a2baf 100644
--- a/src/conference/session/call-session-p.h
+++ b/src/conference/session/call-session-p.h
@@ -67,6 +67,9 @@ public:
 	virtual void updating (bool isUpdate);
 
 	void setCallSessionListener (CallSessionListener *listener) { this->listener = listener; }
+	CallSessionListener *getCallSessionListener()const{
+		return listener;
+	}
 
 protected:
 	void init ();
diff --git a/src/conference/session/call-session.cpp b/src/conference/session/call-session.cpp
index e68a04f26cc4dad33f3766101a9ab0c932c69239..b952e6bc36212ac1868625df1bcc4949e57fa098 100644
--- a/src/conference/session/call-session.cpp
+++ b/src/conference/session/call-session.cpp
@@ -989,7 +989,7 @@ void CallSession::configure (LinphoneCallDir direction, LinphoneProxyConfig *cfg
 		d->startPing();
 	} else if (direction == LinphoneCallIncoming) {
 		d->setParams(new CallSessionParams());
-		d->params->initDefault(getCore());
+		d->params->initDefault(getCore(), LinphoneCallIncoming);
 	}
 }
 
diff --git a/src/conference/session/media-description-renderer.cpp b/src/conference/session/media-description-renderer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ceb6ff77ebbc954d9e4600d3cafe35c1d044af56
--- /dev/null
+++ b/src/conference/session/media-description-renderer.cpp
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#include "media-description-renderer.h"
+
+LINPHONE_BEGIN_NAMESPACE
+
+void OfferAnswerContext::scopeStreamToIndex(size_t index) const{
+	streamIndex = index;
+	localStreamDescription = localMediaDescription ? &localMediaDescription->streams[index] : nullptr;
+	remoteStreamDescription = remoteMediaDescription ? &remoteMediaDescription->streams[index] : nullptr;
+	resultStreamDescription = resultMediaDescription ? &resultMediaDescription->streams[index] : nullptr;
+}
+
+void OfferAnswerContext::dupFrom(const OfferAnswerContext &ctx){
+	OfferAnswerContext oldCtx = *this; // Transfers *this to a temporary object.
+	localMediaDescription = ctx.localMediaDescription ? sal_media_description_ref(ctx.localMediaDescription) : nullptr;
+	remoteMediaDescription = ctx.remoteMediaDescription ? sal_media_description_ref(const_cast<SalMediaDescription*>(ctx.remoteMediaDescription)) : nullptr;
+	resultMediaDescription = ctx.resultMediaDescription ? sal_media_description_ref(const_cast<SalMediaDescription*>(ctx.resultMediaDescription)) : nullptr;
+	localIsOfferer = ctx.localIsOfferer;
+	mOwnsMediaDescriptions = true;
+	// if the temporary oldCtx owns media descriptions, they will be unrefed by the destructor here.
+}
+
+void OfferAnswerContext::copyFrom(const OfferAnswerContext &ctx){
+	OfferAnswerContext oldCtx = *this; // Transfers *this to a temporary object.
+	localMediaDescription = ctx.localMediaDescription;
+	remoteMediaDescription = ctx.remoteMediaDescription;
+	resultMediaDescription = ctx.resultMediaDescription;
+	localIsOfferer = ctx.localIsOfferer;
+	// if the temporary oldCtx owns media descriptions, they will be unrefed by the destructor here.
+}
+
+void OfferAnswerContext::scopeStreamToIndexWithDiff(size_t index, const OfferAnswerContext &previousCtx) const{
+	scopeStreamToIndex(index);
+	previousCtx.scopeStreamToIndex(index);
+	
+	if (previousCtx.localMediaDescription){
+		localStreamDescriptionChanges = sal_media_description_global_equals(previousCtx.localMediaDescription, localMediaDescription)
+		| sal_stream_description_equals(previousCtx.localStreamDescription, localStreamDescription);
+	}else localStreamDescriptionChanges = 0;
+	if (previousCtx.resultMediaDescription && resultMediaDescription){
+		resultStreamDescriptionChanges = sal_media_description_global_equals(previousCtx.resultMediaDescription, resultMediaDescription)
+		| sal_stream_description_equals(previousCtx.resultStreamDescription, resultStreamDescription);
+	}else resultStreamDescriptionChanges = 0;
+}
+
+void OfferAnswerContext::clear(){
+	if (mOwnsMediaDescriptions){
+		if (localMediaDescription) sal_media_description_unref(localMediaDescription);
+		if (remoteMediaDescription) sal_media_description_unref(const_cast<SalMediaDescription*>(remoteMediaDescription));
+		if (resultMediaDescription) sal_media_description_unref(const_cast<SalMediaDescription*>(resultMediaDescription));
+	}
+	localMediaDescription = nullptr;
+	remoteMediaDescription = nullptr;
+	resultMediaDescription = nullptr;
+	localStreamDescription = nullptr;
+	remoteStreamDescription = nullptr;
+	resultStreamDescription = nullptr;
+	localStreamDescriptionChanges = 0;
+	resultStreamDescriptionChanges = 0;
+	mOwnsMediaDescriptions = false;
+}
+
+OfferAnswerContext::~OfferAnswerContext(){
+	clear();
+}
+
+LINPHONE_END_NAMESPACE
+
diff --git a/src/conference/session/media-description-renderer.h b/src/conference/session/media-description-renderer.h
new file mode 100644
index 0000000000000000000000000000000000000000..178d2187c36fd9e045a4be326dddd2a6a95313d7
--- /dev/null
+++ b/src/conference/session/media-description-renderer.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef media_description_renderer_h
+#define media_description_renderer_h
+
+#include "call-session.h"
+#include "c-wrapper/internal/c-sal.h"
+
+LINPHONE_BEGIN_NAMESPACE
+
+
+/**
+ * Represents all offer/answer context.
+ * When passed to a Stream object scopeStreamToIndex() must be called to specify the considered stream index, which
+ * initialize the localStreamDescription, remoteStreamDescription, and resultStreamDescription.
+ */
+class OfferAnswerContext{
+public:
+	OfferAnswerContext() = default;
+	SalMediaDescription *localMediaDescription = nullptr;
+	const SalMediaDescription *remoteMediaDescription = nullptr;
+	const SalMediaDescription *resultMediaDescription = nullptr;
+	bool localIsOfferer = false;
+	
+	mutable int localStreamDescriptionChanges = 0;
+	mutable int resultStreamDescriptionChanges = 0;
+	mutable SalStreamDescription *localStreamDescription = nullptr;
+	mutable const SalStreamDescription *remoteStreamDescription = nullptr;
+	mutable const SalStreamDescription *resultStreamDescription = nullptr;
+	mutable size_t streamIndex = 0;
+	
+	void scopeStreamToIndex(size_t index)const;
+	void scopeStreamToIndexWithDiff(size_t index, const OfferAnswerContext &previousCtx)const;
+	/* Copy descriptions from 'ctx', taking ownership of descriptions. */
+	void dupFrom(const OfferAnswerContext &ctx);
+	/* Copy descriptions from 'ctx', NOT taking ownership of descriptions. */
+	void copyFrom(const OfferAnswerContext &ctx);
+	void clear();
+	~OfferAnswerContext();	
+private:
+	OfferAnswerContext(const OfferAnswerContext &other) = default;
+	OfferAnswerContext & operator=(const OfferAnswerContext &other) = default;
+	bool mOwnsMediaDescriptions = false;
+};
+
+/*
+ * Interface for any kind of engine that is responsible to render the streams described by
+ * a SalMediaDescription within the context of an offer-answer.
+ */
+class MediaDescriptionRenderer{
+public:
+	/*
+	 * Request the engine to fill additional information (that it usually controls) into the local media description.
+	 */
+	virtual void fillLocalMediaDescription(OfferAnswerContext & ctx) = 0;
+	/*
+	 * Prepare to run.
+	 */
+	virtual bool prepare() = 0;
+	/*
+	 * Prepare stage is finishing.
+	 */
+	virtual void finishPrepare() = 0;
+	/*
+	 * Render the streams according to offer answer context.
+	 */
+	virtual void render(const OfferAnswerContext & ctx, CallSession::State targetState) = 0;
+	/*
+	 * Called to notify that the session is confirmed (corresponding to SIP ACK).
+	 */
+	virtual void sessionConfirmed(const OfferAnswerContext &ctx) = 0;
+	/*
+	 * Stop rendering streams.
+	 */
+	virtual void stop() = 0;
+	/*
+	 * Release engine's resource, pending object destruction.
+	 */
+	virtual void finish() = 0;
+	virtual ~MediaDescriptionRenderer() = default;
+};
+
+LINPHONE_END_NAMESPACE
+
+#endif
+
diff --git a/src/conference/session/media-session-p.h b/src/conference/session/media-session-p.h
index 752c793d2939787130c8a659c44ab783b97eb2cf..321e366deade9e36be2ff6e31abfc982e16711e3 100644
--- a/src/conference/session/media-session-p.h
+++ b/src/conference/session/media-session-p.h
@@ -24,10 +24,11 @@
 #include <functional>
 
 #include "call-session-p.h"
+#include "ms2-streams.h"
 
 #include "media-session.h"
 #include "port-config.h"
-#include "nat/ice-agent.h"
+#include "nat/ice-service.h"
 #include "nat/stun-client.h"
 
 #include "linphone/call_stats.h"
@@ -36,7 +37,9 @@
 
 LINPHONE_BEGIN_NAMESPACE
 
-class MediaSessionPrivate : public CallSessionPrivate {
+
+class MediaSessionPrivate : public CallSessionPrivate, private IceServiceListener {
+	friend class StreamsGroup;
 public:
 	static int resumeAfterFailedTransfer (void *userData, unsigned int);
 	static bool_t startPendingRefer (void *userData);
@@ -58,20 +61,13 @@ public:
 	void updated (bool isUpdate);
 	void updating (bool isUpdate) override;
 
-	void enableSymmetricRtp (bool value);
-	void oglRender () const;
+	void oglRender ();
 	void sendVfu ();
 
-	void clearIceCheckList (IceCheckList *cl);
-	void deactivateIce ();
-	void prepareStreamsForIceGathering (bool hasVideo);
-	void stopStreamsForIceGathering ();
-
 	int getAf () const { return af; }
 
 	bool getSpeakerMuted () const;
 	void setSpeakerMuted (bool muted);
-	void forceSpeakerMuted (bool muted);
 
 	bool getMicrophoneMuted () const;
 	void setMicrophoneMuted (bool muted);
@@ -83,28 +79,19 @@ public:
 	void setParams (MediaSessionParams *msp);
 	void setRemoteParams (MediaSessionParams *msp);
 
-	IceSession *getIceSession () const { return iceAgent ? iceAgent->getIceSession() : nullptr; }
-
+	IceService &getIceService() const { return streamsGroup->getIceService(); }
 	SalMediaDescription *getLocalDesc () const { return localDesc; }
 
 	unsigned int getAudioStartCount () const;
 	unsigned int getVideoStartCount () const;
 	unsigned int getTextStartCount () const;
-	MediaStream *getMediaStream (LinphoneStreamType type) const;
 	LinphoneNatPolicy *getNatPolicy () const { return natPolicy; }
 
-	int getRtcpPort (LinphoneStreamType type) const;
-	int getRtpPort (LinphoneStreamType type) const;
 	LinphoneCallStats *getStats (LinphoneStreamType type) const;
-	int getStreamIndex (LinphoneStreamType type) const;
-	int getStreamIndex (MediaStream *ms) const;
+	
 	SalCallOp * getOp () const { return op; }
-	MSWebCam *getVideoDevice () const;
 
-	void initializeStreams ();
 	void stopStreams ();
-	void stopStream (SalStreamDescription *streamDesc);
-	void restartStream (SalStreamDescription *streamDesc, int streamIndex, int sdChanged, CallSession::State targetState);
 
 	// Methods used by testers
 	void addLocalDescChangedFlag (int flag) { localDescChanged |= flag; }
@@ -120,54 +107,52 @@ public:
 
 	// Call listener
 	void snapshotTakenCb(void *userdata, struct _MSFilter *f, unsigned int id, void *arg);
-
+	StreamsGroup & getStreamsGroup()const {
+		return *streamsGroup.get();
+	}
+	std::shared_ptr<Participant> getMe () const;
+	void setDtlsFingerprint(const std::string &fingerPrint);
+	const std::string & getDtlsFingerprint()const;
+	bool isEncryptionMandatory () const;
+	MSWebCam *getVideoDevice()const;
+	void performMutualAuthentication();
+	const std::string &getMediaLocalIp()const{ return mediaLocalIp; }
+	void lossOfMediaDetected();
+	/* test function */
+	IceSession *getIceSession()const;
 private:
-	static OrtpJitterBufferAlgorithm jitterBufferNameToAlgo (const std::string &name);
-
-#ifdef VIDEO_ENABLED
-	static void videoStreamEventCb (void *userData, const MSFilter *f, const unsigned int eventId, const void *args);
-#endif // ifdef VIDEO_ENABLED
+	/* IceServiceListener methods:*/
+	virtual void onGatheringFinished(IceService &service) override;
+	virtual void onIceCompleted(IceService &service) override;
+	virtual void onLosingPairsCompleted(IceService &service) override;
+	virtual void onIceRestartNeeded(IceService & service) override;
+	
 #ifdef TEST_EXT_RENDERER
 	static void extRendererCb (void *userData, const MSPicture *local, const MSPicture *remote);
 #endif // ifdef TEST_EXT_RENDERER
-	static void realTimeTextCharacterReceived (void *userData, MSFilter *f, unsigned int id, void *arg);
 	static int sendDtmf (void *data, unsigned int revents);
-	static float aggregateQualityRatings (float audioRating, float videoRating);
 
-	std::shared_ptr<Participant> getMe () const;
 	void setState (CallSession::State newState, const std::string &message) override;
 
-	void computeStreamsIndexes (const SalMediaDescription *md);
-	void fixCallParams (SalMediaDescription *rmd);
+	void assignStreamsIndexesIncoming(const SalMediaDescription *md);
+	void assignStreamsIndexes();
+	int getFirstStreamWithType(const SalMediaDescription *md, SalStreamType type);
+	void fixCallParams (SalMediaDescription *rmd, bool fromOffer);
 	void initializeParamsAccordingToIncomingCallParams () override;
 	void setCompatibleIncomingCallParams (SalMediaDescription *md);
 	void updateBiggestDesc (SalMediaDescription *md);
 	void updateRemoteSessionIdAndVer ();
 
-	void initStats (LinphoneCallStats *stats, LinphoneStreamType type);
-	void notifyStatsUpdated (int streamIndex);
-
-	OrtpEvQueue *getEventQueue (int streamIndex) const;
-	MediaStream *getMediaStream (int streamIndex) const;
-
-	void fillMulticastMediaAddresses ();
-	int selectFixedPort (int streamIndex, std::pair<int, int> portRange);
-	int selectRandomPort (int streamIndex, std::pair<int, int> portRange);
-	void setPortConfig (int streamIndex, std::pair<int, int> portRange);
-	void setPortConfigFromRtpSession (int streamIndex, RtpSession *session);
-	void setRandomPortConfig (int streamIndex);
 
 	void discoverMtu (const Address &remoteAddr);
-	std::string getBindIpForStream (int streamIndex);
 	void getLocalIp (const Address &remoteAddr);
-	std::string getPublicIpForStream (int streamIndex);
 	void runStunTestsIfNeeded ();
 	void selectIncomingIpVersion ();
 	void selectOutgoingIpVersion ();
 
 	void forceStreamsDirAccordingToState (SalMediaDescription *md);
 	bool generateB64CryptoKey (size_t keyLength, char *keyOut, size_t keyOutSize);
-	void makeLocalMediaDescription ();
+	void makeLocalMediaDescription (bool localIsOfferer);
 	int setupEncryptionKey (SalSrtpCryptoAlgo *crypto, MSCryptoSuite suite, unsigned int tag);
 	void setupDtlsKeys (SalMediaDescription *md);
 	void setupEncryptionKeys (SalMediaDescription *md);
@@ -177,98 +162,31 @@ private:
 	void setupImEncryptionEngineParameters (SalMediaDescription *md);
 	void transferAlreadyAssignedPayloadTypes (SalMediaDescription *oldMd, SalMediaDescription *md);
 	void updateLocalMediaDescriptionFromIce ();
-
-	SalMulticastRole getMulticastRole (SalStreamType type);
-	void joinMulticastGroup (int streamIndex, MediaStream *ms);
-
-	void configureRtpSession(RtpSession *session, LinphoneStreamType type);
-	void setDtlsFingerprint (MSMediaStreamSessions *sessions, const SalStreamDescription *sd, const SalStreamDescription *remote);
-	void setDtlsFingerprintOnAllStreams ();
-	void setDtlsFingerprintOnAudioStream ();
-	void setDtlsFingerprintOnVideoStream ();
-	void setDtlsFingerprintOnTextStream ();
-	void setupDtlsParams (MediaStream *ms);
-	void setZrtpCryptoTypesParameters (MSZrtpParams *params);
-	void startDtls (MSMediaStreamSessions *sessions, const SalStreamDescription *sd, const SalStreamDescription *remote);
-	//To give a chance for auxilary secret to be used, primary channel (I.E audio) should be started either on 200ok if ZRTP is signaled by a zrtp-hash or when ACK is received in case calling side does not have zrtp-hash.
-	void startZrtpPrimaryChannel (const SalStreamDescription *remote);
 	void startDtlsOnAllStreams ();
-	void startDtlsOnAudioStream ();
-	void startDtlsOnTextStream ();
-	void startDtlsOnVideoStream ();
-	void updateStreamCryptoParameters (SalStreamDescription *oldStream, SalStreamDescription *newStream);
-	void updateStreamsCryptoParameters (SalMediaDescription *oldMd, SalMediaDescription *newMd);
-	bool updateCryptoParameters (const SalStreamDescription *localStreamDesc, SalStreamDescription *oldStream, SalStreamDescription *newStream, MediaStream *ms);
-
-	int getIdealAudioBandwidth (const SalMediaDescription *md, const SalStreamDescription *desc);
-	int getVideoBandwidth (const SalMediaDescription *md, const SalStreamDescription *desc);
-	RtpProfile *makeProfile (const SalMediaDescription *md, const SalStreamDescription *desc, int *usedPt);
-	void unsetRtpProfile (int streamIndex);
-	void updateAllocatedAudioBandwidth (const PayloadType *pt, int maxbw);
-
-	void applyJitterBufferParams (RtpSession *session, LinphoneStreamType type);
-	void clearEarlyMediaDestination (MediaStream *ms);
-	void clearEarlyMediaDestinations ();
-	void configureAdaptiveRateControl (MediaStream *ms, const OrtpPayloadType *pt, bool videoWillBeUsed);
-	void configureRtpSessionForRtcpFb (const SalStreamDescription *stream);
-	void configureRtpSessionForRtcpXr (SalStreamType type);
-	RtpSession *createAudioRtpIoSession ();
-	RtpSession *createVideoRtpIoSession ();
+
 	void freeResources ();
-	void handleIceEvents (OrtpEvent *ev);
-	void handleStreamEvents (int streamIndex);
-	void initializeAudioStream ();
-	void initializeTextStream ();
-	void initializeVideoStream ();
 	void prepareEarlyMediaForking ();
-	void postConfigureAudioStreams (bool muted);
-	void setSymmetricRtp (bool value);
-	void setStreamSymmetricRtp(bool value, int streamIndex);
-	void setupRingbackPlayer ();
-	void startAudioStream (CallSession::State targetState);
-	void startStreams (CallSession::State targetState);
-	void startStream (SalStreamDescription *streamDesc, int streamIndex, CallSession::State targetState);
-	void startTextStream ();
-	void startVideoStream (CallSession::State targetState);
-	void stopAudioStream ();
-	void stopTextStream ();
-	void stopVideoStream ();
 	void tryEarlyMediaForking (SalMediaDescription *md);
 	void updateStreamFrozenPayloads (SalStreamDescription *resultDesc, SalStreamDescription *localStreamDesc);
 	void updateFrozenPayloads (SalMediaDescription *result);
-	void updateAudioStream (SalMediaDescription *newMd, CallSession::State targetState);
 	void updateStreams (SalMediaDescription *newMd, CallSession::State targetState);
-	void updateTextStream (SalMediaDescription *newMd, CallSession::State targetState);
-	void updateVideoStream (SalMediaDescription *newMd, CallSession::State targetState);
-	void updateStreamDestination (SalMediaDescription *newMd, SalStreamDescription *newDesc);
-	void updateStreamsDestinations (SalMediaDescription *oldMd, SalMediaDescription *newMd);
 
 	bool allStreamsAvpfEnabled () const;
 	bool allStreamsEncrypted () const;
 	bool atLeastOneStreamStarted () const;
-	void audioStreamAuthTokenReady (const std::string &authToken, bool verified);
-	void audioStreamEncryptionChanged (bool encrypted);
 	uint16_t getAvpfRrInterval () const;
 	unsigned int getNbActiveStreams () const;
-	bool isEncryptionMandatory () const;
-	int mediaParametersChanged (SalMediaDescription *oldMd, SalMediaDescription *newMd);
 	void addSecurityEventInChatrooms (const IdentityAddress &faultyDevice, ConferenceSecurityEvent::SecurityEventType securityEventType);
 	void propagateEncryptionChanged ();
 
-	void fillLogStats (MediaStream *st);
-	void updateLocalStats (LinphoneCallStats *stats, MediaStream *stream) const;
-	void updateRtpStats (LinphoneCallStats *stats, int streamIndex);
-
 	void executeBackgroundTasks (bool oneSecondElapsed);
-	void reportBandwidth ();
-	void reportBandwidthForStream (MediaStream *ms, LinphoneStreamType type);
 
 	void abort (const std::string &errorMsg) override;
 	void handleIncomingReceivedStateInIncomingNotification () override;
-	bool isReadyForInvite () const override;
 	LinphoneStatus pause ();
 	int restartInvite () override;
 	void setTerminated () override;
+	void startAccept();
 	LinphoneStatus startAcceptUpdate (CallSession::State nextState, const std::string &stateInfo) override;
 	LinphoneStatus startUpdate (const std::string &subject = "") override;
 	void terminate () override;
@@ -280,6 +198,7 @@ private:
 	void refreshSockets ();
 	void reinviteToRecoverFromConnectionLoss () override;
 	void repairByInviteWithReplaces () override;
+	void addStreamToBundle(SalMediaDescription *md, SalStreamDescription *sd, const char *mid);
 
 #ifdef VIDEO_ENABLED
 	void videoStreamEventCb (const MSFilter *f, const unsigned int eventId, const void *args);
@@ -288,38 +207,22 @@ private:
 	int sendDtmf ();
 
 	void stunAuthRequestedCb (const char *realm, const char *nonce, const char **username, const char **password, const char **ha1);
+	Stream *getStream(LinphoneStreamType type)const;
 
 private:
 	static const std::string ecStateStore;
 	static const int ecStateMaxLen;
+	static constexpr const int rtpExtHeaderMidNumber = 1;
 
 	std::weak_ptr<Participant> me;
-
-	AudioStream *audioStream = nullptr;
-	OrtpEvQueue *audioStreamEvQueue = nullptr;
-	LinphoneCallStats *audioStats = nullptr;
-	RtpProfile *audioProfile = nullptr;
-	RtpProfile *rtpIoAudioProfile = nullptr;
-	int mainAudioStreamIndex = LINPHONE_CALL_STATS_AUDIO;
-
-	VideoStream *videoStream = nullptr;
-	OrtpEvQueue *videoStreamEvQueue = nullptr;
-	LinphoneCallStats *videoStats = nullptr;
-	RtpProfile *rtpIoVideoProfile = nullptr;
-	RtpProfile *videoProfile = nullptr;
-	int mainVideoStreamIndex = LINPHONE_CALL_STATS_VIDEO;
-	void *videoWindowId = nullptr;
-	bool cameraEnabled = true;
-
-	TextStream *textStream = nullptr;
-	OrtpEvQueue *textStreamEvQueue = nullptr;
-	LinphoneCallStats *textStats = nullptr;
-	RtpProfile *textProfile = nullptr;
-	int mainTextStreamIndex = LINPHONE_CALL_STATS_TEXT;
+	
+	std::unique_ptr<StreamsGroup> streamsGroup;
+	int mainAudioStreamIndex = -1;
+	int mainVideoStreamIndex = -1;
+	int mainTextStreamIndex = -1;
 
 	LinphoneNatPolicy *natPolicy = nullptr;
 	std::unique_ptr<StunClient> stunClient;
-	std::unique_ptr<IceAgent> iceAgent;
 
 	std::vector<std::function<void()>> postProcessHooks;
 
@@ -330,49 +233,26 @@ private:
 	belle_sip_source_t *dtmfTimer = nullptr;
 
 	std::string mediaLocalIp;
-	PortConfig mediaPorts[SAL_MEDIA_DESCRIPTION_MAX_STREAMS];
-
-	// The rtp, srtp, zrtp contexts for each stream.
-	MSMediaStreamSessions sessions[SAL_MEDIA_DESCRIPTION_MAX_STREAMS];
 
 	SalMediaDescription *localDesc = nullptr;
 	int localDescChanged = 0;
 	SalMediaDescription *biggestDesc = nullptr;
 	SalMediaDescription *resultDesc = nullptr;
 	bool expectMediaInAck = false;
+	int freeStreamIndex = 0;
 	unsigned int remoteSessionId = 0;
 	unsigned int remoteSessionVer = 0;
 
-	std::string authToken;
-	bool authTokenVerified = false;
 	std::string dtlsCertificateFingerprint;
 
-	bool forceStreamsReconstruction = false;
-
-	unsigned int audioStartCount = 0;
-	unsigned int videoStartCount = 0;
-	unsigned int textStartCount = 0;
-
 	// Upload bandwidth setting at the time the call is started. Used to detect if it changes during a call.
 	int upBandwidth = 0;
 
-	// Upload bandwidth used by audio.
-	int audioBandwidth = 0;
-
-	bool speakerMuted = false;
-	bool microphoneMuted = false;
-
-	bool audioMuted = false;
-	bool videoMuted = false;
+	bool forceStreamsReconstruction = false;
 	bool automaticallyPaused = false;
 	bool pausedByApp = false;
-	bool recordActive = false;
 	bool incomingIceReinvitePending = false;
-
-	MSSndCard *currentCaptureCard = nullptr;
-	MSSndCard *currentPlayCard = nullptr;
-
-	std::string onHoldFile;
+	bool callAcceptanceDefered = false;
 
 	L_DECLARE_PUBLIC(MediaSession);
 };
diff --git a/src/conference/session/media-session.cpp b/src/conference/session/media-session.cpp
index dc37985a03627c19b336f763d933378013d19ed9..dd92cb70b87a03412e41e6c873c91fbee7c7d706 100644
--- a/src/conference/session/media-session.cpp
+++ b/src/conference/session/media-session.cpp
@@ -17,8 +17,8 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include <iomanip>
-#include <math.h>
+//#include <iomanip>
+//#include <math.h>
 
 #include "address/address-p.h"
 #include "call/call-p.h"
@@ -42,8 +42,7 @@
 #include <mediastreamer2/msequalizer.h>
 #include <mediastreamer2/mseventqueue.h>
 #include <mediastreamer2/msfileplayer.h>
-#include <mediastreamer2/msjpegwriter.h>
-#include <mediastreamer2/msogl.h>
+
 #include <mediastreamer2/msrtt4103.h>
 #include <mediastreamer2/msvolume.h>
 #include <ortp/b64.h>
@@ -71,6 +70,15 @@ const int MediaSessionPrivate::ecStateMaxLen = 1048576; /* 1Mo */
 
 // =============================================================================
 
+
+void MediaSessionPrivate::setDtlsFingerprint(const std::string &fingerPrint){
+	dtlsCertificateFingerprint = fingerPrint;
+}
+
+const std::string & MediaSessionPrivate::getDtlsFingerprint()const{
+	return dtlsCertificateFingerprint;
+}
+
 void MediaSessionPrivate::stunAuthRequestedCb (void *userData, const char *realm, const char *nonce, const char **username, const char **password, const char **ha1) {
 	MediaSessionPrivate *msp = reinterpret_cast<MediaSessionPrivate *>(userData);
 	msp->stunAuthRequestedCb(realm, nonce, username, password, ha1);
@@ -83,6 +91,34 @@ void MediaSessionPrivate::accepted () {
 	CallSessionPrivate::accepted();
 	LinphoneTaskList tl;
 	linphone_task_list_init(&tl);
+	
+	switch (state){
+		case CallSession::State::OutgoingProgress:
+		case CallSession::State::OutgoingRinging:
+		case CallSession::State::OutgoingEarlyMedia:
+		case CallSession::State::Connected:
+			if (q->getCore()->getCCore()->sip_conf.sdp_200_ack){
+				lInfo() << "Initializing local media description according to remote offer in 200Ok";
+				// We were waiting for an incoming offer. Now prepare the local media description according to remote offer.
+				initializeParamsAccordingToIncomingCallParams();
+				makeLocalMediaDescription(op->getRemoteMediaDescription() ? false : true);
+				/*
+				 * If ICE is enabled, we'll have to do the prepare() step, however since defering the sending of the ACK is complicated and 
+				 * confusing from a signaling standpoint, ICE we will skip the STUN gathering by not giving enough time
+				 * for the gathering step. Only local candidates will be answered in the ACK.
+				 */
+				if (getStreamsGroup().prepare()){
+					lWarning() << "Some gathering is needed for ICE, however since a defered sending of ACK is not supported"
+						" the ICE gathering will only contain local candidates.";
+				}
+				getStreamsGroup().finishPrepare();
+				updateLocalMediaDescriptionFromIce();
+			}
+		break;
+		default:
+		break;
+	}
+	
 	/* Reset the internal call update flag, so it doesn't risk to be copied and used in further re-INVITEs */
 	getParams()->getPrivate()->setInternalCallUpdate(false);
 	SalMediaDescription *rmd = op->getRemoteMediaDescription();
@@ -95,10 +131,6 @@ void MediaSessionPrivate::accepted () {
 		md = nullptr;
 	if (md) {
 		/* There is a valid SDP in the response, either offer or answer, and we're able to start/update the streams */
-		if (rmd) {
-			/* Handle remote ICE attributes if any. */
-			iceAgent->updateFromRemoteMediaDescription(localDesc, rmd, !op->isOfferer());
-		}
 		CallSession::State nextState = CallSession::State::Idle;
 		string nextStateMsg;
 		switch (state) {
@@ -141,9 +173,9 @@ void MediaSessionPrivate::accepted () {
 			lError() << "BUG: nextState is not set in accepted(), current state is " << Utils::toString(state);
 		else {
 			updateRemoteSessionIdAndVer();
-			iceAgent->updateIceStateInCallStats();
+			//getIceAgent().updateIceStateInCallStats();
 			updateStreams(md, nextState);
-			fixCallParams(rmd);
+			fixCallParams(rmd, false);
 			setState(nextState, nextStateMsg);
 		}
 	} else { /* Invalid or no SDP */
@@ -179,7 +211,6 @@ void MediaSessionPrivate::accepted () {
 }
 
 void MediaSessionPrivate::ackReceived (LinphoneHeaders *headers) {
-	L_Q();
 	CallSessionPrivate::ackReceived(headers);
 	if (expectMediaInAck) {
 		switch (state) {
@@ -192,15 +223,7 @@ void MediaSessionPrivate::ackReceived (LinphoneHeaders *headers) {
 		}
 		accepted();
 	}
-	if (linphone_core_media_encryption_supported(q->getCore()->getCCore(), LinphoneMediaEncryptionZRTP)) {
-		SalMediaDescription *remote = op->getRemoteMediaDescription();
-		const SalStreamDescription *remoteStream = remote?sal_media_description_find_best_stream(remote, SalAudio):NULL;
-		//Start zrtp if remote has not offered it but local is configured for zrtp and not offerer
-		if (remoteStream && getParams()->getMediaEncryption() == LinphoneMediaEncryptionZRTP && !op->isOfferer() && remoteStream->haveZrtpHash == 0) {
-			lInfo() << "Starting zrtp late";
-			startZrtpPrimaryChannel(remoteStream);
-		}
-	}
+	getStreamsGroup().sessionConfirmed(getStreamsGroup().getCurrentOfferAnswerContext());
 }
 
 void MediaSessionPrivate::dtmfReceived (char dtmf) {
@@ -222,7 +245,7 @@ bool MediaSessionPrivate::failure () {
 			if ((state == CallSession::State::OutgoingInit) || (state == CallSession::State::OutgoingProgress)
 				|| (state == CallSession::State::OutgoingRinging) /* Push notification case */ || (state == CallSession::State::OutgoingEarlyMedia)) {
 				for (int i = 0; i < localDesc->nb_streams; i++) {
-					if (!sal_stream_description_active(&localDesc->streams[i]))
+					if (!sal_stream_description_enabled(&localDesc->streams[i]))
 						continue;
 					if (getParams()->getMediaEncryption() == LinphoneMediaEncryptionSRTP) {
 						if (getParams()->avpfEnabled()) {
@@ -263,13 +286,11 @@ bool MediaSessionPrivate::failure () {
 			&MediaSessionPrivate::resumeAfterFailedTransfer, referer.get(),
 			"Automatic CallSession resuming after failed transfer");
 	}
-
 	q->getCore()->getPrivate()->getToneManager()->stop(q->getSharedFromThis());
 
 	if (ei->reason != SalReasonNone)
 		q->getCore()->getPrivate()->getToneManager()->startErrorTone(q->getSharedFromThis(), linphone_reason_from_sal(ei->reason));
 
-	stopStreams();
 	return false;
 }
 
@@ -298,26 +319,22 @@ void MediaSessionPrivate::remoteRinging () {
 		/* Initialize the remote call params by invoking linphone_call_get_remote_params(). This is useful as the SDP may not be present in the 200Ok */
 		q->getRemoteParams();
 		/* Accept early media */
-		if ((audioStream && audio_stream_started(audioStream))
-#ifdef VIDEO_ENABLED
-			|| (videoStream && video_stream_started(videoStream))
-#endif
-			) {
-			/* Streams already started */
-			tryEarlyMediaForking(md);
-#ifdef VIDEO_ENABLED
-			if (videoStream)
-				video_stream_send_vfu(videoStream); /* Request for iframe */
-#endif
+		
+		if (getStreamsGroup().isStarted()){
+			OfferAnswerContext ctx;
+			ctx.localMediaDescription = localDesc;
+			ctx.resultMediaDescription = md;
+			ctx.remoteMediaDescription = rmd;
+			getStreamsGroup().tryEarlyMediaForking(ctx);
 			return;
 		}
 
 		setState(CallSession::State::OutgoingEarlyMedia, "Early media");
 		q->getCore()->getPrivate()->getToneManager()->stop(q->getSharedFromThis());
 		lInfo() << "Doing early media...";
-		iceAgent->updateFromRemoteMediaDescription(localDesc, rmd, !op->isOfferer());
 		updateStreams(md, state);
-		if ((q->getCurrentParams()->getAudioDirection() == LinphoneMediaDirectionInactive) && audioStream) {
+
+		if ((q->getCurrentParams()->getAudioDirection() == LinphoneMediaDirectionInactive)) {
 			q->getCore()->getPrivate()->getToneManager()->startRingbackTone(q->getSharedFromThis());
 		}
 	} else {
@@ -404,19 +421,19 @@ void MediaSessionPrivate::updated (bool isUpdate) {
 
 
 
-void MediaSessionPrivate::updating (bool isUpdate) {
+void MediaSessionPrivate::updating(bool isUpdate) {
 	L_Q();
 	SalMediaDescription *rmd = op->getRemoteMediaDescription();
-	fixCallParams(rmd);
+	fixCallParams(rmd, true);
 	if (state != CallSession::State::Paused) {
 		/* Refresh the local description, but in paused state, we don't change anything. */
 		if (!rmd && lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "sip", "sdp_200_ack_follow_video_policy", 0)) {
 			lInfo() << "Applying default policy for offering SDP on CallSession [" << q << "]";
 			setParams(new MediaSessionParams());
-			params->initDefault(q->getCore());
+			// Yes we init parameters as if we were in the case of an outgoing call, because it is a resume with no SDP.
+			params->initDefault(q->getCore(), LinphoneCallOutgoing);
 		}
-		makeLocalMediaDescription();
-		op->setLocalMediaDescription(localDesc);
+		makeLocalMediaDescription(false);
 	}
 	if (rmd) {
 		SalErrorInfo sei;
@@ -451,122 +468,43 @@ void MediaSessionPrivate::updating (bool isUpdate) {
 
 // -----------------------------------------------------------------------------
 
-void MediaSessionPrivate::enableSymmetricRtp (bool value) {
-	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (sessions[i].rtp_session)
-			rtp_session_set_symmetric_rtp(sessions[i].rtp_session, value);
-	}
-}
 
-void MediaSessionPrivate::oglRender () const {
+void MediaSessionPrivate::oglRender () {
 #ifdef VIDEO_ENABLED
-	if (videoStream && videoStream->output && (ms_filter_get_id(videoStream->output) == MS_OGL_ID))
-		ms_filter_call_method(videoStream->output, MS_OGL_RENDER, nullptr);
+	if (mainVideoStreamIndex != -1){
+		MS2VideoStream * vs = dynamic_cast<MS2VideoStream*>(getStreamsGroup().getStream(mainVideoStreamIndex));
+		if (vs) vs->oglRender();
+	}
 #endif
 }
 
 void MediaSessionPrivate::sendVfu () {
-#ifdef VIDEO_ENABLED
-	if (videoStream)
-		video_stream_send_vfu(videoStream);
-#endif
-}
-
-// -----------------------------------------------------------------------------
-
-void MediaSessionPrivate::clearIceCheckList (IceCheckList *cl) {
-	if (audioStream && audioStream->ms.ice_check_list == cl)
-		audioStream->ms.ice_check_list = nullptr;
-	if (videoStream && videoStream->ms.ice_check_list == cl)
-		videoStream->ms.ice_check_list = nullptr;
-	if (textStream && textStream->ms.ice_check_list == cl)
-		textStream->ms.ice_check_list = nullptr;
-}
-
-void MediaSessionPrivate::deactivateIce () {
-	if (audioStream)
-		audioStream->ms.ice_check_list = nullptr;
-	if (videoStream)
-		videoStream->ms.ice_check_list = nullptr;
-	if (textStream)
-		textStream->ms.ice_check_list = nullptr;
-	_linphone_call_stats_set_ice_state(audioStats, LinphoneIceStateNotActivated);
-	_linphone_call_stats_set_ice_state(videoStats, LinphoneIceStateNotActivated);
-	_linphone_call_stats_set_ice_state(textStats, LinphoneIceStateNotActivated);
-	stopStreamsForIceGathering();
-}
-
-void MediaSessionPrivate::prepareStreamsForIceGathering (bool hasVideo) {
-	if (audioStream->ms.state == MSStreamInitialized)
-		audio_stream_prepare_sound(audioStream, nullptr, nullptr);
-#ifdef VIDEO_ENABLED
-	if (hasVideo && videoStream && (videoStream->ms.state == MSStreamInitialized))
-		video_stream_prepare_video(videoStream);
-#endif
-	if (getParams()->realtimeTextEnabled() && (textStream->ms.state == MSStreamInitialized))
-		text_stream_prepare_text(textStream);
+	getStreamsGroup().forEach<VideoControlInterface>([](VideoControlInterface *i){ i->sendVfu(); });
 }
 
-void MediaSessionPrivate::stopStreamsForIceGathering () {
-	if (audioStream && (audioStream->ms.state == MSStreamPreparing))
-		audio_stream_unprepare_sound(audioStream);
-#ifdef VIDEO_ENABLED
-	if (videoStream && (videoStream->ms.state == MSStreamPreparing))
-		video_stream_unprepare_video(videoStream);
-#endif
-	if (textStream && (textStream->ms.state == MSStreamPreparing))
-		text_stream_unprepare_text(textStream);
-}
 
 // -----------------------------------------------------------------------------
 
 bool MediaSessionPrivate::getSpeakerMuted () const {
-	return speakerMuted;
+	AudioControlInterface *i = getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	return i ? !i->speakerEnabled() : false;
 }
 
 void MediaSessionPrivate::setSpeakerMuted (bool muted) {
-	if (speakerMuted == muted)
-		return;
-	speakerMuted = muted;
-
-	if (state == CallSession::State::StreamsRunning)
-		forceSpeakerMuted(speakerMuted);
-}
-
-void MediaSessionPrivate::forceSpeakerMuted (bool muted) {
-	L_Q();
-
-	if (!audioStream)
-		return;
-
-	if (muted)
-		audio_stream_set_spk_gain(audioStream, 0);
-	else
-		audio_stream_set_spk_gain_db(audioStream, q->getCore()->getCCore()->sound_conf.soft_play_lev);
+	AudioControlInterface *i = getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	if (i) i->enableSpeaker(!muted);
 }
 
 // -----------------------------------------------------------------------------
 
 bool MediaSessionPrivate::getMicrophoneMuted () const {
-	return microphoneMuted;
+	AudioControlInterface *i = getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	return i ? !i->micEnabled() : false;
 }
 
 void MediaSessionPrivate::setMicrophoneMuted (bool muted) {
-	L_Q();
-
-	if (microphoneMuted == muted)
-		return;
-	microphoneMuted = muted;
-
-	if (!audioStream)
-		return;
-
-	if (state == CallSession::State::StreamsRunning) {
-		if (microphoneMuted)
-			audio_stream_set_mic_gain(audioStream, 0);
-		else
-			audio_stream_set_mic_gain_db(audioStream, q->getCore()->getCCore()->sound_conf.soft_mic_lev);
-	}
+	AudioControlInterface *i = getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	if (i) i->enableMic(muted);
 }
 
 // -----------------------------------------------------------------------------
@@ -589,203 +527,36 @@ void MediaSessionPrivate::setRemoteParams (MediaSessionParams *msp) {
 	remoteParams = msp;
 }
 
-MediaStream *MediaSessionPrivate::getMediaStream (LinphoneStreamType type) const {
-	return getMediaStream(int(type));
-}
-
-int MediaSessionPrivate::getRtcpPort (LinphoneStreamType type) const  {
-	return mediaPorts[getStreamIndex(getMediaStream(type))].rtcpPort;
-}
-
-int MediaSessionPrivate::getRtpPort (LinphoneStreamType type) const {
-	return mediaPorts[getStreamIndex(getMediaStream(type))].rtpPort;
-}
-
-LinphoneCallStats * MediaSessionPrivate::getStats (LinphoneStreamType type) const {
+Stream *MediaSessionPrivate::getStream(LinphoneStreamType type)const{
 	switch (type) {
 		case LinphoneStreamTypeAudio:
-			return audioStats;
+			return getStreamsGroup().lookupMainStream(SalAudio);
 		case LinphoneStreamTypeVideo:
-			return videoStats;
+			return getStreamsGroup().lookupMainStream(SalVideo);
 		case LinphoneStreamTypeText:
-			return textStats;
+			return getStreamsGroup().lookupMainStream(SalText);
 		case LinphoneStreamTypeUnknown:
-		default:
-			return nullptr;
+		break;
 	}
+	return nullptr;
 }
 
-int MediaSessionPrivate::getStreamIndex (LinphoneStreamType type) const {
-	return getStreamIndex(getMediaStream(type));
-}
-
-int MediaSessionPrivate::getStreamIndex (MediaStream *ms) const {
-	if (ms == &audioStream->ms)
-		return mainAudioStreamIndex;
-	else if (ms == &videoStream->ms)
-		return mainVideoStreamIndex;
-	else if (ms == &textStream->ms)
-		return mainTextStreamIndex;
-	return -1;
-}
-
-MSWebCam * MediaSessionPrivate::getVideoDevice () const {
-	L_Q();
-	bool paused = (state == CallSession::State::Pausing) || (state == CallSession::State::Paused);
-	if (paused || videoMuted || !cameraEnabled)
-#ifdef VIDEO_ENABLED
-		return ms_web_cam_manager_get_cam(ms_factory_get_web_cam_manager(q->getCore()->getCCore()->factory),
-			"StaticImage: Static picture");
-#else
-		return nullptr;
-#endif
-	else
-		return q->getCore()->getCCore()->video_conf.device;
+LinphoneCallStats * MediaSessionPrivate::getStats(LinphoneStreamType type) const {
+	Stream *s = getStream(type);
+	if (s) return s->getStats();
+	lError() << "There is no stats for main stream of type " << linphone_stream_type_to_string(type) << " because this stream doesn't exist.";
+	return nullptr;
 }
 
 // -----------------------------------------------------------------------------
 
-void MediaSessionPrivate::initializeStreams () {
-	initializeAudioStream();
-	initializeVideoStream();
-	initializeTextStream();
-}
-
-void MediaSessionPrivate::stopStream (SalStreamDescription *streamDesc) {
-	L_Q();
-
-	if (streamDesc->type == SalAudio && audioStream) {
-		if (videoStream)
-			audio_stream_unlink_video(audioStream, videoStream);
-		stopAudioStream();
-
-		if (q->getCore()->getCCore()->msevq)
-			ms_event_queue_skip(q->getCore()->getCCore()->msevq);
-
-		if (audioProfile) {
-			rtp_profile_destroy(audioProfile);
-			audioProfile = nullptr;
-			unsetRtpProfile(mainAudioStreamIndex);
-		}
-
-		if (rtpIoAudioProfile) {
-			rtp_profile_destroy(rtpIoAudioProfile);
-			rtpIoAudioProfile = nullptr;
-		}
-
-		q->getCore()->soundcardHintCheck();
-	} else if (streamDesc->type == SalVideo && videoStream) {
-		if (audioStream)
-			audio_stream_unlink_video(audioStream, videoStream);
-		stopVideoStream();
-
-		if (q->getCore()->getCCore()->msevq)
-			ms_event_queue_skip(q->getCore()->getCCore()->msevq);
-
-		if (videoProfile) {
-			rtp_profile_destroy(videoProfile);
-			videoProfile = nullptr;
-			unsetRtpProfile(mainVideoStreamIndex);
-		}
-
-		if (rtpIoVideoProfile) {
-			rtp_profile_destroy(rtpIoVideoProfile);
-			rtpIoVideoProfile = nullptr;
-		}
-	} else if (streamDesc->type == SalText && textStream) {
-		stopTextStream();
-
-		if (q->getCore()->getCCore()->msevq)
-			ms_event_queue_skip(q->getCore()->getCCore()->msevq);
-
-		if (textProfile) {
-			rtp_profile_destroy(textProfile);
-			textProfile = nullptr;
-			unsetRtpProfile(mainTextStreamIndex);
-		}
-	}
-}
 
 void MediaSessionPrivate::stopStreams () {
 	L_Q();
-	if (audioStream || videoStream || textStream) {
-		if (audioStream && videoStream)
-			audio_stream_unlink_video(audioStream, videoStream);
-		stopAudioStream();
-		stopVideoStream();
-		stopTextStream();
-		if (q->getCore()->getCCore()->msevq)
-			ms_event_queue_skip(q->getCore()->getCCore()->msevq);
-	}
-
-	if (audioProfile) {
-		rtp_profile_destroy(audioProfile);
-		audioProfile = nullptr;
-		unsetRtpProfile(mainAudioStreamIndex);
-	}
-	if (videoProfile) {
-		rtp_profile_destroy(videoProfile);
-		videoProfile = nullptr;
-		unsetRtpProfile(mainVideoStreamIndex);
-	}
-	if (textProfile) {
-		rtp_profile_destroy(textProfile);
-		textProfile = nullptr;
-		unsetRtpProfile(mainTextStreamIndex);
-	}
-	if (rtpIoAudioProfile) {
-		rtp_profile_destroy(rtpIoAudioProfile);
-		rtpIoAudioProfile = nullptr;
-	}
-	if (rtpIoVideoProfile) {
-		rtp_profile_destroy(rtpIoVideoProfile);
-		rtpIoVideoProfile = nullptr;
-	}
-
+	if (getStreamsGroup().isStarted()) getStreamsGroup().stop();
 	q->getCore()->soundcardHintCheck();
 }
 
-void MediaSessionPrivate::restartStream (SalStreamDescription *streamDesc, int streamIndex, int sdChanged, CallSession::State targetState) {
-	L_Q();
-	string streamTypeName = sal_stream_description_get_type_as_string(streamDesc);
-
-	stopStream(streamDesc);
-
-	if (streamDesc->type == SalAudio) {
-		if (sdChanged & SAL_MEDIA_DESCRIPTION_NETWORK_XXXCAST_CHANGED) {
-			lInfo() << "Media ip type has changed, destroying sessions context on CallSession [" << q << "] for " << streamTypeName << " stream";
-			ms_media_stream_sessions_uninit(&sessions[mainAudioStreamIndex]);
-		}
-
-		initializeAudioStream();
-	} else if (streamDesc->type == SalVideo) {
-		if (sdChanged & SAL_MEDIA_DESCRIPTION_NETWORK_XXXCAST_CHANGED) {
-			lInfo() << "Media ip type has changed, destroying sessions context on CallSession [" << q << "] for " << streamTypeName << " stream";
-			ms_media_stream_sessions_uninit(&sessions[mainVideoStreamIndex]);
-		}
-
-		initializeVideoStream();
-	} else if (streamDesc->type == SalText) {
-		if (sdChanged & SAL_MEDIA_DESCRIPTION_NETWORK_XXXCAST_CHANGED) {
-			lInfo() << "Media ip type has changed, destroying sessions context on CallSession [" << q << "] for " << streamTypeName << " stream";
-			ms_media_stream_sessions_uninit(&sessions[mainTextStreamIndex]);
-		}
-
-		initializeTextStream();
-	}
-
-	if (getParams()->earlyMediaSendingEnabled() && (state == CallSession::State::OutgoingEarlyMedia)) {
-		if (streamDesc->type == SalAudio && audioStream)
-			rtp_session_set_symmetric_rtp(audioStream->ms.sessions.rtp_session, false);
-		else if (streamDesc->type == SalVideo && videoStream)
-			rtp_session_set_symmetric_rtp(videoStream->ms.sessions.rtp_session, false);
-	}
-
-	startStream(streamDesc, streamIndex, targetState);
-
-	updateStreamFrozenPayloads(streamDesc, &localDesc->streams[streamIndex]);
-}
-
 // -----------------------------------------------------------------------------
 
 void MediaSessionPrivate::onNetworkReachable (bool sipNetworkReachable, bool mediaNetworkReachable) {
@@ -802,19 +573,6 @@ void MediaSessionPrivate::onNetworkReachable (bool sipNetworkReachable, bool med
 
 // -----------------------------------------------------------------------------
 
-OrtpJitterBufferAlgorithm MediaSessionPrivate::jitterBufferNameToAlgo (const string &name) {
-	if (name == "basic") return OrtpJitterBufferBasic;
-	if (name == "rls") return OrtpJitterBufferRecursiveLeastSquare;
-	lError() << "Invalid jitter buffer algorithm: " << name;
-	return OrtpJitterBufferRecursiveLeastSquare;
-}
-
-#ifdef VIDEO_ENABLED
-void MediaSessionPrivate::videoStreamEventCb (void *userData, const MSFilter *f, const unsigned int eventId, const void *args) {
-	MediaSessionPrivate *msp = reinterpret_cast<MediaSessionPrivate *>(userData);
-	msp->videoStreamEventCb(f, eventId, args);
-}
-#endif
 
 #ifdef TEST_EXT_RENDERER
 void MediaSessionPrivate::extRendererCb (void *userData, const MSPicture *local, const MSPicture *remote) {
@@ -823,10 +581,6 @@ void MediaSessionPrivate::extRendererCb (void *userData, const MSPicture *local,
 }
 #endif
 
-void MediaSessionPrivate::realTimeTextCharacterReceived (void *userData, MSFilter *f, unsigned int id, void *arg) {
-	MediaSessionPrivate *msp = reinterpret_cast<MediaSessionPrivate *>(userData);
-	msp->realTimeTextCharacterReceived(f, id, arg);
-}
 
 int MediaSessionPrivate::sendDtmf (void *data, unsigned int revents) {
 	MediaSession *session = reinterpret_cast<MediaSession *>(data);
@@ -835,18 +589,7 @@ int MediaSessionPrivate::sendDtmf (void *data, unsigned int revents) {
 
 // -----------------------------------------------------------------------------
 
-float MediaSessionPrivate::aggregateQualityRatings (float audioRating, float videoRating) {
-	float result;
-	if ((audioRating < 0) && (videoRating < 0))
-		result = -1;
-	else if (audioRating < 0)
-		result = videoRating * 5.0f;
-	else if (videoRating < 0)
-		result = audioRating * 5.0f;
-	else
-		result = audioRating * videoRating * 5.0f;
-	return result;
-}
+
 
 // -----------------------------------------------------------------------------
 
@@ -875,7 +618,7 @@ void MediaSessionPrivate::setState (CallSession::State newState, const string &m
 			// Handle specifically the case of an incoming ICE-concluded reINVITE
 			lInfo() << "Checking for ICE reINVITE";
 			rmd = op->getRemoteMediaDescription();
-			if (iceAgent && rmd && iceAgent->checkIceReinviteNeedsDeferedResponse(rmd)) {
+			if (rmd && getIceService().reinviteNeedsDeferedResponse(rmd)) {
 				deferUpdate = true;
 				deferUpdateInternal = true;
 				incomingIceReinvitePending = true;
@@ -889,106 +632,47 @@ void MediaSessionPrivate::setState (CallSession::State newState, const string &m
 
 // -----------------------------------------------------------------------------
 
-void MediaSessionPrivate::computeStreamsIndexes (const SalMediaDescription *md) {
-	bool audioFound = false;
-	bool videoFound = false;
-	bool textFound = false;
-	for (int i = 0; i < md->nb_streams; i++) {
-		if (md->streams[i].type == SalAudio) {
-			if (audioFound)
-				lInfo() << "audio stream index found: " << i << ", but main audio stream already set to " << mainAudioStreamIndex;
-			else {
-				mainAudioStreamIndex = i;
-				audioFound = true;
-				lInfo() << "audio stream index found: " << i << ", updating main audio stream index";
-			}
-			/* Check that the default value of a another stream doesn't match the new one */
-			if (i == mainVideoStreamIndex) {
-				for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; j++) {
-					if (sal_stream_description_active(&md->streams[j]))
-						continue;
-					if ((j != mainVideoStreamIndex) && (j != mainTextStreamIndex)) {
-						lInfo() << i << " was used for video stream ; now using " << j;
-						mainVideoStreamIndex = j;
-						break;
-					}
-				}
-			}
-			if (i == mainTextStreamIndex) {
-				for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; j++) {
-					if (sal_stream_description_active(&md->streams[j]))
-						continue;
-					if ((j != mainVideoStreamIndex) && (j != mainTextStreamIndex)) {
-						lInfo() << i << " was used for text stream ; now using " << j;
-						mainTextStreamIndex = j;
-						break;
-					}
-				}
-			}
-		} else if (md->streams[i].type == SalVideo) {
-			if (videoFound)
-				lInfo() << "video stream index found: " << i << ", but main video stream already set to " << mainVideoStreamIndex;
-			else {
-				mainVideoStreamIndex = i;
-				videoFound = true;
-				lInfo() << "video stream index found: " << i << ", updating main video stream index";
-			}
-			/* Check that the default value of a another stream doesn't match the new one */
-			if (i == mainAudioStreamIndex) {
-				for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; j++) {
-					if (sal_stream_description_active(&md->streams[j]))
-						continue;
-					if ((j != mainAudioStreamIndex) && (j != mainTextStreamIndex)) {
-						lInfo() << i << " was used for audio stream ; now using " << j;
-						mainAudioStreamIndex = j;
-						break;
-					}
-				}
-			}
-			if (i == mainTextStreamIndex) {
-				for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; j++) {
-					if (sal_stream_description_active(&md->streams[j]))
-						continue;
-					if ((j != mainAudioStreamIndex) && (j != mainTextStreamIndex)) {
-						lInfo() << i << " was used for text stream ; now using " << j;
-						mainTextStreamIndex = j;
-						break;
-					}
-				}
-			}
-		} else if (md->streams[i].type == SalText) {
-			if (textFound)
-				lInfo() << "text stream index found: " << i << ", but main text stream already set to " << mainTextStreamIndex;
-			else {
-				mainTextStreamIndex = i;
-				textFound = true;
-				lInfo() << "text stream index found: " << i << ", updating main text stream index";
-			}
-			/* Check that the default value of a another stream doesn't match the new one */
-			if (i == mainAudioStreamIndex) {
-				for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; j++) {
-					if (sal_stream_description_active(&md->streams[j]))
-						continue;
-					if ((j != mainVideoStreamIndex) && (j != mainAudioStreamIndex)) {
-						lInfo() << i << " was used for audio stream ; now using " << j;
-						mainAudioStreamIndex = j;
-						break;
-					}
-				}
-			}
-			if (i == mainVideoStreamIndex) {
-				for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; j++) {
-					if (sal_stream_description_active(&md->streams[j]))
-						continue;
-					if ((j != mainVideoStreamIndex) && (j != mainAudioStreamIndex)) {
-						lInfo() << i << " was used for video stream ; now using " << j;
-						mainVideoStreamIndex = j;
-						break;
-					}
-				}
-			}
-		}
+
+int MediaSessionPrivate::getFirstStreamWithType(const SalMediaDescription *md, SalStreamType type){
+	int i;
+	for (i = 0; i < md->nb_streams; ++i) {
+		if (md->streams[i].type == type) return i;
+	}
+	return -1;
+}
+
+void MediaSessionPrivate::assignStreamsIndexes(){
+	if (biggestDesc && freeStreamIndex < biggestDesc->nb_streams) freeStreamIndex = biggestDesc->nb_streams;
+	
+	/*Initialize stream indexes from potential incoming offer.*/
+	SalMediaDescription *rmd = op ? op->getRemoteMediaDescription() : nullptr;
+	if (rmd) assignStreamsIndexesIncoming(rmd);
+	
+	/*Assign indexes for our streams, if no incoming offer was received, or if new streams are requested.*/
+	if (getParams()->audioEnabled() && mainAudioStreamIndex == -1){
+		mainAudioStreamIndex = freeStreamIndex++;
+	}
+	if (getParams()->videoEnabled() && mainVideoStreamIndex == -1){
+		mainVideoStreamIndex = freeStreamIndex++;
+	}
+	if (getParams()->realtimeTextEnabled() && mainTextStreamIndex == -1){
+		mainTextStreamIndex = freeStreamIndex++;
+	}
+	lInfo() << "Stream indexes selected (-1 = unassigned): mainAudioStreamIndex=" << mainAudioStreamIndex <<
+		", mainVideoStreamIndex=" << mainVideoStreamIndex << ", mainTextStreamIndex=" << mainTextStreamIndex;
+}
+
+void MediaSessionPrivate::assignStreamsIndexesIncoming(const SalMediaDescription *md) {
+	if (mainAudioStreamIndex == -1){
+		mainAudioStreamIndex = getFirstStreamWithType(md, SalAudio);
+	}
+	if (mainVideoStreamIndex == -1){
+		mainVideoStreamIndex = getFirstStreamWithType(md, SalVideo);
 	}
+	if (mainTextStreamIndex == -1){
+		mainTextStreamIndex = getFirstStreamWithType(md, SalText);
+	}
+	if (freeStreamIndex < md->nb_streams) freeStreamIndex = md->nb_streams;
 }
 
 /*
@@ -997,59 +681,57 @@ void MediaSessionPrivate::computeStreamsIndexes (const SalMediaDescription *md)
  * - the video enablement parameter according to what is offered and our local policy.
  * Fixing the params to proper values avoid request video by accident during internal call updates, pauses and resumes
  */
-void MediaSessionPrivate::fixCallParams (SalMediaDescription *rmd) {
+void MediaSessionPrivate::fixCallParams (SalMediaDescription *rmd, bool fromOffer) {
 	L_Q();
-	if (rmd) {
-		computeStreamsIndexes(rmd);
-		updateBiggestDesc(rmd);
-		/* Why disabling implicit_rtcp_fb ? It is a local policy choice actually. It doesn't disturb to propose it again and again
-		 * even if the other end apparently doesn't support it.
-		 * The following line of code is causing trouble, while for example making an audio call, then adding video.
-		 * Due to the 200Ok response of the audio-only offer where no rtcp-fb attribute is present, implicit_rtcp_fb is set to
-		 * false, which is then preventing it to be eventually used when video is later added to the call.
-		 * I did the choice of commenting it out.
-		 */
-		/*params.getPrivate()->enableImplicitRtcpFb(params.getPrivate()->implicitRtcpFbEnabled() & sal_media_description_has_implicit_avpf(rmd));*/
-	}
+	if (!rmd) return;
+	
+	updateBiggestDesc(rmd);
+	/* Why disabling implicit_rtcp_fb ? It is a local policy choice actually. It doesn't disturb to propose it again and again
+		* even if the other end apparently doesn't support it.
+		* The following line of code is causing trouble, while for example making an audio call, then adding video.
+		* Due to the 200Ok response of the audio-only offer where no rtcp-fb attribute is present, implicit_rtcp_fb is set to
+		* false, which is then preventing it to be eventually used when video is later added to the call.
+		* I did the choice of commenting it out.
+		*/
+	/*params.getPrivate()->enableImplicitRtcpFb(params.getPrivate()->implicitRtcpFbEnabled() & sal_media_description_has_implicit_avpf(rmd));*/
 	const MediaSessionParams *rcp = q->getRemoteParams();
 	if (rcp) {
-		if (getParams()->audioEnabled() && !rcp->audioEnabled()) {
-			lInfo() << "CallSession [" << q << "]: disabling audio in our call params because the remote doesn't want it";
-			getParams()->enableAudio(false);
-		}
-		if (getParams()->videoEnabled() && !rcp->videoEnabled()) {
-			lInfo() << "CallSession [" << q << "]: disabling video in our call params because the remote doesn't want it";
-			getParams()->enableVideo(false);
+		if (!fromOffer){
+			/*
+			 * This is to avoid to re-propose again some streams that have just been declined.
+			 */
+			if (getParams()->audioEnabled() && !rcp->audioEnabled()) {
+				lInfo() << "CallSession [" << q << "]: disabling audio in our call params because the remote doesn't want it";
+				getParams()->enableAudio(false);
+			}
+			if (getParams()->videoEnabled() && !rcp->videoEnabled()) {
+				lInfo() << "CallSession [" << q << "]: disabling video in our call params because the remote doesn't want it";
+				getParams()->enableVideo(false);
+			}
+			if (getParams()->realtimeTextEnabled() && !rcp->realtimeTextEnabled()) {
+				lInfo() << "CallSession [" << q << "]: disabling RTT in our call params because the remote doesn't want it";
+				getParams()->enableRealtimeText(false);
+			}
 		}
+		// Real Time Text is always by default accepted when proposed.
+		if (!getParams()->realtimeTextEnabled() && rcp->realtimeTextEnabled())
+			getParams()->enableRealtimeText(true);
+		
 		if (rcp->videoEnabled() && q->getCore()->getCCore()->video_policy.automatically_accept && linphone_core_video_enabled(q->getCore()->getCCore()) && !getParams()->videoEnabled()) {
 			lInfo() << "CallSession [" << q << "]: re-enabling video in our call params because the remote wants it and the policy allows to automatically accept";
 			getParams()->enableVideo(true);
 		}
-		if (rcp->realtimeTextEnabled() && !getParams()->realtimeTextEnabled())
-			getParams()->enableRealtimeText(true);
+		
 	}
 }
 
 void MediaSessionPrivate::initializeParamsAccordingToIncomingCallParams () {
-	L_Q();
 	CallSessionPrivate::initializeParamsAccordingToIncomingCallParams();
-	getCurrentParams()->getPrivate()->setUpdateCallWhenIceCompleted(getParams()->getPrivate()->getUpdateCallWhenIceCompleted());
-	getParams()->enableVideo(linphone_core_video_enabled(q->getCore()->getCCore()) && q->getCore()->getCCore()->video_policy.automatically_accept);
 	SalMediaDescription *md = op->getRemoteMediaDescription();
 	if (md) {
+		assignStreamsIndexesIncoming(md);
 		/* It is licit to receive an INVITE without SDP, in this case WE choose the media parameters according to policy */
 		setCompatibleIncomingCallParams(md);
-		/* Set multicast role & address if any */
-		if (!op->isOfferer()) {
-			for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-				if (md->streams[i].dir == SalStreamInactive)
-					continue;
-				if ((md->streams[i].rtp_addr[0] != '\0') && ms_is_multicast(md->streams[i].rtp_addr)) {
-					md->streams[i].multicast_role = SalMulticastReceiver;
-					mediaPorts[i].multicastIp = md->streams[i].rtp_addr;
-				}
-			}
-		}
 	}
 }
 
@@ -1079,18 +761,35 @@ void MediaSessionPrivate::setCompatibleIncomingCallParams (SalMediaDescription *
 		if (!mandatory || (mandatory && linphone_core_get_media_encryption(lc) == LinphoneMediaEncryptionNone))
 			getParams()->setMediaEncryption(LinphoneMediaEncryptionNone);
 	}
+	if (mainAudioStreamIndex != -1){
+		SalStreamDescription *sd = &md->streams[mainAudioStreamIndex];
+		const char *rtpAddr = (sd->rtp_addr[0] != '\0') ? sd->rtp_addr : md->addr;
+		if (ms_is_multicast(rtpAddr)){
+			lInfo() << "Incoming offer has audio multicast, enabling it in local params.";
+			getParams()->enableAudioMulticast(true);
+		}else getParams()->enableAudioMulticast(false);
+	}
+	if (mainVideoStreamIndex != -1){
+		SalStreamDescription *sd = &md->streams[mainVideoStreamIndex];
+		const char *rtpAddr = (sd->rtp_addr[0] != '\0') ? sd->rtp_addr : md->addr;
+		if (ms_is_multicast(rtpAddr)){
+			lInfo() << "Incoming offer has video multicast, enabling it in local params.";
+			getParams()->enableVideoMulticast(true);
+		}else getParams()->enableVideoMulticast(false);
+	}
+	
 	/* In case of nat64, even ipv4 addresses are reachable from v6. Should be enhanced to manage stream by stream connectivity (I.E v6 or v4) */
 	/*if (!sal_media_description_has_ipv6(md)){
 		lInfo() << "The remote SDP doesn't seem to offer any IPv6 connectivity, so disabling IPv6 for this call";
 		af = AF_INET;
 	}*/
-	fixCallParams(md);
+	fixCallParams(md, true);
 }
 
 void MediaSessionPrivate::updateBiggestDesc (SalMediaDescription *md) {
 	if (!biggestDesc || (md->nb_streams > biggestDesc->nb_streams)) {
 		/* We have been offered and now are ready to proceed, or we added a new stream,
-		 * store the media description to remember the mapping of calls */
+		 * store the media description to remember the mapping of streams within this call. */
 		if (biggestDesc) {
 			sal_media_description_unref(biggestDesc);
 			biggestDesc = nullptr;
@@ -1109,214 +808,42 @@ void MediaSessionPrivate::updateRemoteSessionIdAndVer () {
 
 // -----------------------------------------------------------------------------
 
-void MediaSessionPrivate::initStats (LinphoneCallStats *stats, LinphoneStreamType type) {
-	_linphone_call_stats_set_type(stats, type);
-	_linphone_call_stats_set_received_rtcp(stats, nullptr);
-	_linphone_call_stats_set_sent_rtcp(stats, nullptr);
-	_linphone_call_stats_set_ice_state(stats, LinphoneIceStateNotActivated);
-}
 
-void MediaSessionPrivate::notifyStatsUpdated (int streamIndex) {
-	L_Q();
-	LinphoneCallStats *stats = nullptr;
-	if (streamIndex == mainAudioStreamIndex)
-		stats = audioStats;
-	else if (streamIndex == mainVideoStreamIndex)
-		stats = videoStats;
-	else if (streamIndex == mainTextStreamIndex)
-		stats = textStats;
-	else
-		return;
-	if (_linphone_call_stats_get_updated(stats)) {
-		switch (_linphone_call_stats_get_updated(stats)) {
-			case LINPHONE_CALL_STATS_RECEIVED_RTCP_UPDATE:
-			case LINPHONE_CALL_STATS_SENT_RTCP_UPDATE:
-				if (listener) {
-					listener->onRtcpUpdateForReporting(q->getSharedFromThis(),
-						(streamIndex == mainAudioStreamIndex)
-							? SalAudio
-							: (streamIndex == mainVideoStreamIndex) ? SalVideo : SalText
-					);
-				}
-				break;
-			default:
-				break;
-		}
-		if (listener)
-			listener->onStatsUpdated(q->getSharedFromThis(), stats);
-		_linphone_call_stats_set_updated(stats, 0);
-	}
-}
 
 // -----------------------------------------------------------------------------
 
-OrtpEvQueue * MediaSessionPrivate::getEventQueue (int streamIndex) const {
-	if (streamIndex == mainAudioStreamIndex)
-		return audioStreamEvQueue;
-	if (streamIndex == mainVideoStreamIndex)
-		return videoStreamEvQueue;
-	if (streamIndex == mainTextStreamIndex)
-		return textStreamEvQueue;
-	lError() << "getEventQueue(): no stream index " << streamIndex;
-	return nullptr;
-}
 
 unsigned int MediaSessionPrivate::getAudioStartCount () const {
-	return audioStartCount;
+	Stream *s = getStreamsGroup().lookupMainStream(SalAudio);
+	return s ? (unsigned int)s->getStartCount() : 0;
 }
 
 unsigned int MediaSessionPrivate::getVideoStartCount () const {
-	return videoStartCount;
+	Stream *s = getStreamsGroup().lookupMainStream(SalVideo);
+	return s ? (unsigned int)s->getStartCount() : 0;
 }
 
 unsigned int MediaSessionPrivate::getTextStartCount () const {
-	return textStartCount;
-}
-
-MediaStream *MediaSessionPrivate::getMediaStream (int streamIndex) const {
-	if (streamIndex == mainAudioStreamIndex)
-		return audioStream ? &audioStream->ms : nullptr;
-	if (streamIndex == mainVideoStreamIndex)
-		return videoStream ? &videoStream->ms : nullptr;
-	if (streamIndex == mainTextStreamIndex)
-		return textStream ? &textStream->ms : nullptr;
-	lError() << "getMediaStream(): no stream index " << streamIndex;
-	return nullptr;
+	Stream *s = getStreamsGroup().lookupMainStream(SalText);
+	return s ? (unsigned int)s->getStartCount() : 0;
 }
 
 // -----------------------------------------------------------------------------
 
-void MediaSessionPrivate::fillMulticastMediaAddresses () {
-	L_Q();
-	if (getParams()->audioMulticastEnabled())
-		mediaPorts[mainAudioStreamIndex].multicastIp = linphone_core_get_audio_multicast_addr(q->getCore()->getCCore());
-	else
-		mediaPorts[mainAudioStreamIndex].multicastIp.clear();
-	if (getParams()->videoMulticastEnabled())
-		mediaPorts[mainVideoStreamIndex].multicastIp = linphone_core_get_video_multicast_addr(q->getCore()->getCCore());
-	else
-		mediaPorts[mainVideoStreamIndex].multicastIp.clear();
-}
+// -----------------------------------------------------------------------------
 
-int MediaSessionPrivate::selectFixedPort (int streamIndex, pair<int, int> portRange) {
+void MediaSessionPrivate::discoverMtu (const Address &remoteAddr) {
 	L_Q();
-	for (int triedPort = portRange.first; triedPort < (portRange.first + 100); triedPort += 2) {
-		bool alreadyUsed = false;
-		for (const bctbx_list_t *elem = linphone_core_get_calls(q->getCore()->getCCore()); elem != nullptr; elem = bctbx_list_next(elem)) {
-			LinphoneCall *lcall = reinterpret_cast<LinphoneCall *>(bctbx_list_get_data(elem));
-			shared_ptr<MediaSession> session = static_pointer_cast<MediaSession>(L_GET_CPP_PTR_FROM_C_OBJECT(lcall)->getPrivate()->getActiveSession());
-			int existingPort = session->getPrivate()->mediaPorts[streamIndex].rtpPort;
-			if (existingPort == triedPort) {
-				alreadyUsed = true;
-				break;
-			}
+	if (q->getCore()->getCCore()->net_conf.mtu == 0) {
+		/* Attempt to discover mtu */
+		int mtu = ms_discover_mtu(remoteAddr.getDomain().c_str());
+		if (mtu > 0) {
+			ms_factory_set_mtu(q->getCore()->getCCore()->factory, mtu);
+			lInfo() << "Discovered mtu is " << mtu << ", RTP payload max size is " << ms_factory_get_payload_max_size(q->getCore()->getCCore()->factory);
 		}
-		if (!alreadyUsed)
-			return triedPort;
 	}
-
-	lError() << "Could not find any free port !";
-	return -1;
 }
 
-int MediaSessionPrivate::selectRandomPort (int streamIndex, pair<int, int> portRange) {
-	L_Q();
-	unsigned int rangeSize = static_cast<unsigned int>(portRange.second - portRange.first);
-	
-	for (int nbTries = 0; nbTries < 100; nbTries++) {
-		bool alreadyUsed = false;
-		unsigned int randomInRangeSize = (bctbx_random() % rangeSize) & (unsigned int)~0x1; /* Select an even number */
-		int triedPort = ((int)randomInRangeSize) + portRange.first;
-		/*If portRange.first is even, the triedPort will be even too. The one who configures a port range that starts with an odd number will
-		 * get odd RTP port numbers.*/
-		
-		for (const bctbx_list_t *elem = linphone_core_get_calls(q->getCore()->getCCore()); elem != nullptr; elem = bctbx_list_next(elem)) {
-			LinphoneCall *lcall = reinterpret_cast<LinphoneCall *>(bctbx_list_get_data(elem));
-			shared_ptr<MediaSession> session = static_pointer_cast<MediaSession>(L_GET_CPP_PTR_FROM_C_OBJECT(lcall)->getPrivate()->getActiveSession());
-			int existingPort = session->getPrivate()->mediaPorts[streamIndex].rtpPort;
-			if (existingPort == triedPort) {
-				alreadyUsed = true;
-				break;
-			}
-		}
-		lInfo() << "Port " << triedPort << " randomly taken from range [ " << portRange.first << " , " << portRange.second << "]";
-		if (!alreadyUsed)
-			return triedPort;
-	}
-
-	lError() << "Could not find any free port!";
-	return -1;
-}
-
-void MediaSessionPrivate::setPortConfig(int streamIndex, pair<int, int> portRange) {
-	if ((portRange.first <= 0) && (portRange.second <= 0)) {
-		setRandomPortConfig(streamIndex);
-	} else {
-		if (portRange.first == portRange.second) {
-			/* Fixed port */
-			int port = selectFixedPort(streamIndex, portRange);
-			if (port == -1) {
-				setRandomPortConfig(streamIndex);
-				return;
-			}
-			mediaPorts[streamIndex].rtpPort = port;
-		} else {
-			/* Select random port in the specified range */
-			mediaPorts[streamIndex].rtpPort = selectRandomPort(streamIndex, portRange);
-		}
-		mediaPorts[streamIndex].rtcpPort = mediaPorts[streamIndex].rtpPort + 1;
-	}
-}
-
-void MediaSessionPrivate::setPortConfigFromRtpSession (int streamIndex, RtpSession *session) {
-	mediaPorts[streamIndex].rtpPort = rtp_session_get_local_port(session);
-	mediaPorts[streamIndex].rtcpPort = rtp_session_get_local_rtcp_port(session);
-}
-
-void MediaSessionPrivate::setRandomPortConfig (int streamIndex) {
-	mediaPorts[streamIndex].rtpPort = -1;
-	mediaPorts[streamIndex].rtcpPort = -1;
-}
-
-// -----------------------------------------------------------------------------
-
-void MediaSessionPrivate::discoverMtu (const Address &remoteAddr) {
-	L_Q();
-	if (q->getCore()->getCCore()->net_conf.mtu == 0) {
-		/* Attempt to discover mtu */
-		int mtu = ms_discover_mtu(remoteAddr.getDomain().c_str());
-		if (mtu > 0) {
-			ms_factory_set_mtu(q->getCore()->getCCore()->factory, mtu);
-			lInfo() << "Discovered mtu is " << mtu << ", RTP payload max size is " << ms_factory_get_payload_max_size(q->getCore()->getCCore()->factory);
-		}
-	}
-}
-
-string MediaSessionPrivate::getBindIpForStream (int streamIndex) {
-	L_Q();
-	string bindIp = lp_config_get_string(linphone_core_get_config(q->getCore()->getCCore()), "rtp", "bind_address", "");
-	PortConfig *pc = &mediaPorts[streamIndex];
-	if (!pc->multicastIp.empty()){
-		if (direction == LinphoneCallOutgoing) {
-			/* As multicast sender, we must decide a local interface to use to send multicast, and bind to it */
-			char multicastBindIp[LINPHONE_IPADDR_SIZE];
-			memset(multicastBindIp, 0, sizeof(multicastBindIp));
-			linphone_core_get_local_ip_for((pc->multicastIp.find_first_of(':') == string::npos) ? AF_INET : AF_INET6, nullptr, multicastBindIp);
-			bindIp = pc->multicastBindIp = multicastBindIp;
-		} else {
-			/* Otherwise we shall use an address family of the same family of the multicast address, because
-			 * dual stack socket and multicast don't work well on Mac OS (linux is OK, as usual). */
-			bindIp = (pc->multicastIp.find_first_of(':') == string::npos) ? "0.0.0.0" : "::0";
-		}
-	}else if (bindIp.empty()){
-		/*If ipv6 is not enabled, for listening to 0.0.0.0. The default behavior of mediastreamer when no IP is passed is to try ::0, and in
-		 * case of failure try 0.0.0.0 . But we don't want this if IPv6 is explicitely disabled.*/
-		if (!linphone_core_ipv6_enabled(q->getCore()->getCCore())){
-			bindIp = "0.0.0.0";
-		}
-	}
-	return bindIp;
-}
 
 /**
  * Fill the local ip that routes to the internet according to the destination, or guess it by other special means.
@@ -1377,17 +904,14 @@ void MediaSessionPrivate::getLocalIp (const Address &remoteAddr) {
 	}
 }
 
-string MediaSessionPrivate::getPublicIpForStream (int streamIndex) {
-	if (!mediaPorts[streamIndex].multicastIp.empty())
-		return mediaPorts[streamIndex].multicastIp;
-	return mediaLocalIp;
-}
-
 void MediaSessionPrivate::runStunTestsIfNeeded () {
 	L_Q();
 	if (linphone_nat_policy_stun_enabled(natPolicy) && !(linphone_nat_policy_ice_enabled(natPolicy) || linphone_nat_policy_turn_enabled(natPolicy))) {
 		stunClient = makeUnique<StunClient>(q->getCore());
-		int ret = stunClient->run(mediaPorts[mainAudioStreamIndex].rtpPort, mediaPorts[mainVideoStreamIndex].rtpPort, mediaPorts[mainTextStreamIndex].rtpPort);
+		int audioPort = mainAudioStreamIndex ? getStreamsGroup().getStream(mainAudioStreamIndex)->getPortConfig().rtpPort : 0;
+		int videoPort = mainVideoStreamIndex ? getStreamsGroup().getStream(mainVideoStreamIndex)->getPortConfig().rtpPort : 0;
+		int textPort = mainTextStreamIndex ? getStreamsGroup().getStream(mainTextStreamIndex)->getPortConfig().rtpPort : 0;
+		int ret = stunClient->run(audioPort, videoPort, textPort);
 		if (ret >= 0)
 			pingTime = ret;
 	}
@@ -1459,7 +983,7 @@ void MediaSessionPrivate::selectOutgoingIpVersion () {
 
 void MediaSessionPrivate::forceStreamsDirAccordingToState (SalMediaDescription *md) {
 	L_Q();
-	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
+	for (int i = 0; i < md->nb_streams; i++) {
 		SalStreamDescription *sd = &md->streams[i];
 		switch (state) {
 			case CallSession::State::Pausing:
@@ -1510,25 +1034,27 @@ bool MediaSessionPrivate::generateB64CryptoKey (size_t keyLength, char *keyOut,
 	return true;
 }
 
-void MediaSessionPrivate::makeLocalMediaDescription () {
+void MediaSessionPrivate::addStreamToBundle(SalMediaDescription *md, SalStreamDescription *sd, const char *mid){
+	SalStreamBundle *bundle;
+	if (md->bundles == nullptr){
+		bundle = sal_media_description_add_new_bundle(md);
+	}else{
+		bundle = (SalStreamBundle*) md->bundles->data;
+	}
+	sal_stream_bundle_add_stream(bundle, sd, mid);
+	sd->mid_rtp_ext_header_id = rtpExtHeaderMidNumber;
+	/* rtcp-mux must be enabled when bundle mode is proposed.*/
+	sd->rtcp_mux = TRUE;
+}
+
+void MediaSessionPrivate::makeLocalMediaDescription(bool localIsOfferer) {
 	L_Q();
-	int maxIndex = 0;
 	bool rtcpMux = !!lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "rtp", "rtcp_mux", 0);
 	SalMediaDescription *md = sal_media_description_new();
 	SalMediaDescription *oldMd = localDesc;
-
-	/* Multicast is only set in case of outgoing call */
-	if (direction == LinphoneCallOutgoing) {
-		if (getParams()->audioMulticastEnabled()) {
-			md->streams[mainAudioStreamIndex].ttl = linphone_core_get_audio_multicast_ttl(q->getCore()->getCCore());
-			md->streams[mainAudioStreamIndex].multicast_role = SalMulticastSender;
-		}
-		if (getParams()->videoMulticastEnabled()) {
-			md->streams[mainVideoStreamIndex].ttl = linphone_core_get_video_multicast_ttl(q->getCore()->getCCore());
-			md->streams[mainVideoStreamIndex].multicast_role = SalMulticastSender;
-		}
-	}
-
+	
+	assignStreamsIndexes();
+	
 	getParams()->getPrivate()->adaptToNetwork(q->getCore()->getCCore(), pingTime);
 
 	string subject = q->getParams()->getSessionName();
@@ -1539,6 +1065,9 @@ void MediaSessionPrivate::makeLocalMediaDescription () {
 	md->session_id = (oldMd ? oldMd->session_id : (bctbx_random() & 0xfff));
 	md->session_ver = (oldMd ? (oldMd->session_ver + 1) : (bctbx_random() & 0xfff));
 	md->nb_streams = (biggestDesc ? biggestDesc->nb_streams : 1);
+	
+	md->accept_bundles = getParams()->rtpBundleEnabled() || 
+		linphone_config_get_bool(linphone_core_get_config(q->getCore()->getCCore()), "rtp", "accept_bundle", TRUE);
 
 	/* Re-check local ip address each time we make a new offer, because it may change in case of network reconnection */
 	{
@@ -1570,109 +1099,122 @@ void MediaSessionPrivate::makeLocalMediaDescription () {
 	SalCustomSdpAttribute *customSdpAttributes = getParams()->getPrivate()->getCustomSdpAttributes();
 	if (customSdpAttributes)
 		md->custom_sdp_attributes = sal_custom_sdp_attribute_clone(customSdpAttributes);
+	
 
 	PayloadTypeHandler pth(q->getCore());
 
-	bctbx_list_t *l = pth.makeCodecsList(SalAudio, getParams()->getAudioBandwidthLimit(), -1,
-		oldMd ? oldMd->streams[mainAudioStreamIndex].already_assigned_payloads : nullptr);
-	if (l && getParams()->audioEnabled()) {
-		strncpy(md->streams[mainAudioStreamIndex].rtp_addr, getPublicIpForStream(mainAudioStreamIndex).c_str(), sizeof(md->streams[mainAudioStreamIndex].rtp_addr));
-		strncpy(md->streams[mainAudioStreamIndex].rtcp_addr, getPublicIpForStream(mainAudioStreamIndex).c_str(), sizeof(md->streams[mainAudioStreamIndex].rtcp_addr));
-		strncpy(md->streams[mainAudioStreamIndex].name, "Audio", sizeof(md->streams[mainAudioStreamIndex].name) - 1);
-		md->streams[mainAudioStreamIndex].rtp_port = mediaPorts[mainAudioStreamIndex].rtpPort;
-		md->streams[mainAudioStreamIndex].rtcp_port = mediaPorts[mainAudioStreamIndex].rtcpPort;
+	bctbx_list_t *l = NULL;
+	if (mainAudioStreamIndex != -1){
+		l = nullptr;
 		md->streams[mainAudioStreamIndex].proto = getParams()->getMediaProto();
 		md->streams[mainAudioStreamIndex].dir = getParams()->getPrivate()->getSalAudioDirection();
 		md->streams[mainAudioStreamIndex].type = SalAudio;
-		md->streams[mainAudioStreamIndex].rtcp_mux = rtcpMux;
-		int downPtime = getParams()->getPrivate()->getDownPtime();
-		if (downPtime)
-			md->streams[mainAudioStreamIndex].ptime = downPtime;
-		else
-			md->streams[mainAudioStreamIndex].ptime = linphone_core_get_download_ptime(q->getCore()->getCCore());
-		md->streams[mainAudioStreamIndex].max_rate = pth.getMaxCodecSampleRate(l);
-		md->streams[mainAudioStreamIndex].payloads = l;
-		if (audioStream && audioStream->ms.sessions.rtp_session) {
-			md->streams[mainAudioStreamIndex].rtp_ssrc = rtp_session_get_send_ssrc(audioStream->ms.sessions.rtp_session);
+		if (getParams()->audioEnabled() && (l = pth.makeCodecsList(SalAudio, getParams()->getAudioBandwidthLimit(), -1,
+		oldMd ? oldMd->streams[mainAudioStreamIndex].already_assigned_payloads : nullptr))) {
+			strncpy(md->streams[mainAudioStreamIndex].name, "Audio", sizeof(md->streams[mainAudioStreamIndex].name) - 1);
+			md->streams[mainAudioStreamIndex].rtcp_mux = rtcpMux;
+			md->streams[mainAudioStreamIndex].rtp_port = SAL_STREAM_DESCRIPTION_PORT_TO_BE_DETERMINED;
+			int downPtime = getParams()->getPrivate()->getDownPtime();
+			if (downPtime)
+				md->streams[mainAudioStreamIndex].ptime = downPtime;
+			else
+				md->streams[mainAudioStreamIndex].ptime = linphone_core_get_download_ptime(q->getCore()->getCCore());
+			md->streams[mainAudioStreamIndex].max_rate = pth.getMaxCodecSampleRate(l);
+			md->streams[mainAudioStreamIndex].payloads = l;
 			strncpy(md->streams[mainAudioStreamIndex].rtcp_cname, getMe()->getAddress().asString().c_str(), sizeof(md->streams[mainAudioStreamIndex].rtcp_cname));
-		}
-		else
-			lWarning() << "Cannot get audio local ssrc for CallSession [" << q << "]";
-		if (mainAudioStreamIndex > maxIndex)
-			maxIndex = mainAudioStreamIndex;
-	} else {
-		lInfo() << "Don't put audio stream on local offer for CallSession [" << q << "]";
-		md->streams[mainAudioStreamIndex].dir = SalStreamInactive;
-		if(l)
-			l = bctbx_list_free_with_data(l, (bctbx_list_free_func)payload_type_destroy);
-	}
-	SalCustomSdpAttribute *sdpMediaAttributes = getParams()->getPrivate()->getCustomSdpMediaAttributes(LinphoneStreamTypeAudio);
-	if (sdpMediaAttributes)
-		md->streams[mainAudioStreamIndex].custom_sdp_attributes = sal_custom_sdp_attribute_clone(sdpMediaAttributes);
-
-	md->streams[mainVideoStreamIndex].proto = getParams()->getMediaProto();
-	md->streams[mainVideoStreamIndex].dir = getParams()->getPrivate()->getSalVideoDirection();
-	md->streams[mainVideoStreamIndex].type = SalVideo;
-	md->streams[mainVideoStreamIndex].rtcp_mux = rtcpMux;
-	strncpy(md->streams[mainVideoStreamIndex].name, "Video", sizeof(md->streams[mainVideoStreamIndex].name) - 1);
-
-	l = pth.makeCodecsList(SalVideo, 0, -1,
-		oldMd ? oldMd->streams[mainVideoStreamIndex].already_assigned_payloads : nullptr);
-	if (l && getParams()->videoEnabled()){
-		strncpy(md->streams[mainVideoStreamIndex].rtp_addr, getPublicIpForStream(mainVideoStreamIndex).c_str(), sizeof(md->streams[mainVideoStreamIndex].rtp_addr));
-		strncpy(md->streams[mainVideoStreamIndex].rtcp_addr, getPublicIpForStream(mainVideoStreamIndex).c_str(), sizeof(md->streams[mainVideoStreamIndex].rtcp_addr));
-		md->streams[mainVideoStreamIndex].rtp_port = mediaPorts[mainVideoStreamIndex].rtpPort;
-		md->streams[mainVideoStreamIndex].rtcp_port = mediaPorts[mainVideoStreamIndex].rtcpPort;
-		md->streams[mainVideoStreamIndex].payloads = l;
-		if (videoStream && videoStream->ms.sessions.rtp_session) {
-			md->streams[mainVideoStreamIndex].rtp_ssrc = rtp_session_get_send_ssrc(videoStream->ms.sessions.rtp_session);
+			if (getParams()->rtpBundleEnabled()) addStreamToBundle(md, &md->streams[mainAudioStreamIndex], "as");
+			
+			if (getParams()->audioMulticastEnabled()) {
+				md->streams[mainAudioStreamIndex].ttl = linphone_core_get_audio_multicast_ttl(q->getCore()->getCCore());
+				md->streams[mainAudioStreamIndex].multicast_role = (direction == LinphoneCallOutgoing) ? SalMulticastSender : SalMulticastReceiver;
+			}
+			
+		} else {
+			lInfo() << "Don't put audio stream on local offer for CallSession [" << q << "]";
+			md->streams[mainAudioStreamIndex].dir = SalStreamInactive;
+			if(l)
+				l = bctbx_list_free_with_data(l, (bctbx_list_free_func)payload_type_destroy);
+		}
+		customSdpAttributes = getParams()->getPrivate()->getCustomSdpMediaAttributes(LinphoneStreamTypeAudio);
+		if (customSdpAttributes)
+			md->streams[mainAudioStreamIndex].custom_sdp_attributes = sal_custom_sdp_attribute_clone(customSdpAttributes);
+	}
+	if (mainVideoStreamIndex != -1){
+		l = nullptr;
+		md->streams[mainVideoStreamIndex].proto = getParams()->getMediaProto();
+		md->streams[mainVideoStreamIndex].dir = getParams()->getPrivate()->getSalVideoDirection();
+		md->streams[mainVideoStreamIndex].type = SalVideo;
+		
+		if (getParams()->videoEnabled() && (l = pth.makeCodecsList(SalVideo, 0, -1,
+			oldMd ? oldMd->streams[mainVideoStreamIndex].already_assigned_payloads : nullptr)) ){
+			md->streams[mainVideoStreamIndex].rtcp_mux = rtcpMux;
+			md->streams[mainVideoStreamIndex].rtp_port = SAL_STREAM_DESCRIPTION_PORT_TO_BE_DETERMINED;
+			strncpy(md->streams[mainVideoStreamIndex].name, "Video", sizeof(md->streams[mainVideoStreamIndex].name) - 1);
+			md->streams[mainVideoStreamIndex].payloads = l;
 			strncpy(md->streams[mainVideoStreamIndex].rtcp_cname, getMe()->getAddress().asString().c_str(), sizeof(md->streams[mainVideoStreamIndex].rtcp_cname));
-		} else
-			lWarning() << "Cannot get video local ssrc for CallSession [" << q << "]";
-		if (mainVideoStreamIndex > maxIndex)
-			maxIndex = mainVideoStreamIndex;
-	} else {
-		lInfo() << "Don't put video stream on local offer for CallSession [" << q << "]";
-		md->streams[mainVideoStreamIndex].dir = SalStreamInactive;
-		if(l)
-			l = bctbx_list_free_with_data(l, (bctbx_list_free_func)payload_type_destroy);
-	}
-	sdpMediaAttributes = getParams()->getPrivate()->getCustomSdpMediaAttributes(LinphoneStreamTypeVideo);
-	if (sdpMediaAttributes)
-		md->streams[mainVideoStreamIndex].custom_sdp_attributes = sal_custom_sdp_attribute_clone(sdpMediaAttributes);
-
-	md->streams[mainTextStreamIndex].proto = getParams()->getMediaProto();
-	md->streams[mainTextStreamIndex].dir = SalStreamSendRecv;
-	md->streams[mainTextStreamIndex].type = SalText;
-	md->streams[mainTextStreamIndex].rtcp_mux = rtcpMux;
-	strncpy(md->streams[mainTextStreamIndex].name, "Text", sizeof(md->streams[mainTextStreamIndex].name) - 1);
-	if (getParams()->realtimeTextEnabled()) {
-		strncpy(md->streams[mainTextStreamIndex].rtp_addr, getPublicIpForStream(mainTextStreamIndex).c_str(), sizeof(md->streams[mainTextStreamIndex].rtp_addr));
-		strncpy(md->streams[mainTextStreamIndex].rtcp_addr, getPublicIpForStream(mainTextStreamIndex).c_str(), sizeof(md->streams[mainTextStreamIndex].rtcp_addr));
-
-		md->streams[mainTextStreamIndex].rtp_port = mediaPorts[mainTextStreamIndex].rtpPort;
-		md->streams[mainTextStreamIndex].rtcp_port = mediaPorts[mainTextStreamIndex].rtcpPort;
-
-		l = pth.makeCodecsList(SalText, 0, -1,
-			oldMd ? oldMd->streams[mainTextStreamIndex].already_assigned_payloads : nullptr);
-		md->streams[mainTextStreamIndex].payloads = l;
-		if (textStream && textStream->ms.sessions.rtp_session) {
-			md->streams[mainTextStreamIndex].rtp_ssrc = rtp_session_get_send_ssrc(textStream->ms.sessions.rtp_session);
+			if (getParams()->rtpBundleEnabled()) addStreamToBundle(md, &md->streams[mainVideoStreamIndex], "vs");
+			
+			if (getParams()->videoMulticastEnabled()) {
+				md->streams[mainVideoStreamIndex].ttl = linphone_core_get_video_multicast_ttl(q->getCore()->getCCore());
+				md->streams[mainVideoStreamIndex].multicast_role = (direction == LinphoneCallOutgoing) ? SalMulticastSender : SalMulticastReceiver;
+			}
+		} else {
+			lInfo() << "Don't put video stream on local offer for CallSession [" << q << "]";
+			md->streams[mainVideoStreamIndex].dir = SalStreamInactive;
+			if(l)
+				l = bctbx_list_free_with_data(l, (bctbx_list_free_func)payload_type_destroy);
+		}
+		customSdpAttributes = getParams()->getPrivate()->getCustomSdpMediaAttributes(LinphoneStreamTypeVideo);
+		if (customSdpAttributes)
+			md->streams[mainVideoStreamIndex].custom_sdp_attributes = sal_custom_sdp_attribute_clone(customSdpAttributes);
+	}
+
+	if (mainTextStreamIndex != -1){
+		l = nullptr;
+		md->streams[mainTextStreamIndex].proto = getParams()->getMediaProto();
+		md->streams[mainTextStreamIndex].dir = SalStreamSendRecv;
+		md->streams[mainTextStreamIndex].type = SalText;
+		if (getParams()->realtimeTextEnabled() && (l = pth.makeCodecsList(SalText, 0, -1,
+				oldMd ? oldMd->streams[mainTextStreamIndex].already_assigned_payloads : nullptr)) ) {
+			md->streams[mainTextStreamIndex].rtcp_mux = rtcpMux;
+			md->streams[mainTextStreamIndex].rtp_port = getParams()->realtimeTextEnabled() ? SAL_STREAM_DESCRIPTION_PORT_TO_BE_DETERMINED : 0;
+			strncpy(md->streams[mainTextStreamIndex].name, "Text", sizeof(md->streams[mainTextStreamIndex].name) - 1);
+			md->streams[mainTextStreamIndex].payloads = l;
 			strncpy(md->streams[mainTextStreamIndex].rtcp_cname, getMe()->getAddress().asString().c_str(), sizeof(md->streams[mainTextStreamIndex].rtcp_cname));
-		} else
-			lWarning() << "Cannot get text local ssrc for CallSession [" << q << "]";
-		if (mainTextStreamIndex > maxIndex)
-			maxIndex = mainTextStreamIndex;
-	} else {
-		lInfo() << "Don't put text stream on local offer for CallSession [" << q << "]";
-		md->streams[mainTextStreamIndex].dir = SalStreamInactive;
+			if (getParams()->rtpBundleEnabled()) addStreamToBundle(md, &md->streams[mainTextStreamIndex], "ts");
+		} else {
+			lInfo() << "Don't put text stream on local offer for CallSession [" << q << "]";
+			md->streams[mainTextStreamIndex].dir = SalStreamInactive;
+			if(l)
+				l = bctbx_list_free_with_data(l, (bctbx_list_free_func)payload_type_destroy);
+		}
+		customSdpAttributes = getParams()->getPrivate()->getCustomSdpMediaAttributes(LinphoneStreamTypeText);
+		if (customSdpAttributes)
+			md->streams[mainTextStreamIndex].custom_sdp_attributes = sal_custom_sdp_attribute_clone(customSdpAttributes);
 	}
-	sdpMediaAttributes = getParams()->getPrivate()->getCustomSdpMediaAttributes(LinphoneStreamTypeText);
-	if (sdpMediaAttributes)
-		md->streams[mainTextStreamIndex].custom_sdp_attributes = sal_custom_sdp_attribute_clone(sdpMediaAttributes);
 
-	md->nb_streams = MAX(md->nb_streams, maxIndex + 1);
+	md->nb_streams = freeStreamIndex;
 
+	setupEncryptionKeys(md);
+	setupImEncryptionEngineParameters(md);
+	setupRtcpFb(md);
+	setupRtcpXr(md);
+	if (stunClient)
+		stunClient->updateMediaDescription(md);
+	localDesc = md;
+	
+	OfferAnswerContext ctx;
+	ctx.localMediaDescription = localDesc;
+	ctx.remoteMediaDescription = localIsOfferer ? nullptr : ( op ? op->getRemoteMediaDescription() : nullptr);
+	ctx.localIsOfferer = localIsOfferer;
+	/* Now instanciate the streams according to the media description. */
+	getStreamsGroup().createStreams(ctx);
+	if (mainAudioStreamIndex != -1) getStreamsGroup().setStreamMain((size_t)mainAudioStreamIndex);
+	if (mainVideoStreamIndex != -1) getStreamsGroup().setStreamMain((size_t)mainVideoStreamIndex);
+	if (mainTextStreamIndex != -1) getStreamsGroup().setStreamMain((size_t)mainTextStreamIndex);
+	/* Get the transport addresses filled in to the media description. */
+	getStreamsGroup().fillLocalMediaDescription(ctx);
+	
 	/* Deactivate unused streams */
 	for (int i = md->nb_streams; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
 		if (md->streams[i].rtp_port == 0) {
@@ -1683,15 +1225,7 @@ void MediaSessionPrivate::makeLocalMediaDescription () {
 			}
 		}
 	}
-	setupEncryptionKeys(md);
-	setupDtlsKeys(md);
-	setupZrtpHash(md);
-	setupImEncryptionEngineParameters(md);
-	setupRtcpFb(md);
-	setupRtcpXr(md);
-	if (stunClient)
-		stunClient->updateMediaDescription(md);
-	localDesc = md;
+	
 	updateLocalMediaDescriptionFromIce();
 	if (oldMd) {
 		transferAlreadyAssignedPayloadTypes(oldMd, md);
@@ -1707,24 +1241,7 @@ void MediaSessionPrivate::makeLocalMediaDescription () {
 		}
 	}
 	forceStreamsDirAccordingToState(md);
-}
-
-void MediaSessionPrivate::setupDtlsKeys (SalMediaDescription *md) {
-	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&md->streams[i]))
-			continue;
-		/* If media encryption is set to DTLS check presence of fingerprint in the call which shall have been set at stream init
-		 * but it may have failed when retrieving certificate resulting in no fingerprint present and then DTLS not usable */
-		if (sal_stream_description_has_dtls(&md->streams[i])) {
-			/* Get the self fingerprint from call (it's computed at stream init) */
-			strncpy(md->streams[i].dtls_fingerprint, dtlsCertificateFingerprint.c_str(), sizeof(md->streams[i].dtls_fingerprint));
-			/* If we are offering, SDP will have actpass setup attribute when role is unset, if we are responding the result mediadescription will be set to SalDtlsRoleIsClient */
-			md->streams[i].dtls_role = SalDtlsRoleUnset;
-		} else {
-			md->streams[i].dtls_fingerprint[0] = '\0';
-			md->streams[i].dtls_role = SalDtlsRoleInvalid;
-		}
-	}
+	if (op) op->setLocalMediaDescription(localDesc);
 }
 
 int MediaSessionPrivate::setupEncryptionKey (SalSrtpCryptoAlgo *crypto, MSCryptoSuite suite, unsigned int tag) {
@@ -1756,9 +1273,7 @@ int MediaSessionPrivate::setupEncryptionKey (SalSrtpCryptoAlgo *crypto, MSCrypto
 
 void MediaSessionPrivate::setupRtcpFb (SalMediaDescription *md) {
 	L_Q();
-	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&md->streams[i]))
-			continue;
+	for (int i = 0; i < md->nb_streams; i++) {
 		md->streams[i].rtcp_fb.generic_nack_enabled = !!lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "rtp", "rtcp_fb_generic_nack_enabled", 0);
 		md->streams[i].rtcp_fb.tmmbr_enabled = !!lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "rtp", "rtcp_fb_tmmbr_enabled", 1);
 		md->streams[i].implicit_rtcp_fb = getParams()->getPrivate()->implicitRtcpFbEnabled();
@@ -1796,29 +1311,11 @@ void MediaSessionPrivate::setupRtcpXr (SalMediaDescription *md) {
 			md->rtcp_xr.stat_summary_flags = OrtpRtcpXrStatSummaryLoss | OrtpRtcpXrStatSummaryDup | OrtpRtcpXrStatSummaryJitt | OrtpRtcpXrStatSummaryTTL;
 		md->rtcp_xr.voip_metrics_enabled = !!lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "rtp", "rtcp_xr_voip_metrics_enabled", 1);
 	}
-	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&md->streams[i]))
-			continue;
+	for (int i = 0; i < md->nb_streams; i++) {
 		memcpy(&md->streams[i].rtcp_xr, &md->rtcp_xr, sizeof(md->streams[i].rtcp_xr));
 	}
 }
 
-void MediaSessionPrivate::setupZrtpHash (SalMediaDescription *md) {
-	L_Q();
-	if (linphone_core_media_encryption_supported(q->getCore()->getCCore(), LinphoneMediaEncryptionZRTP)) {
-		/* Set the hello hash for all streams */
-		for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-			if (!sal_stream_description_active(&md->streams[i]))
-				continue;
-			if (sessions[i].zrtp_context) {
-				ms_zrtp_getHelloHash(sessions[i].zrtp_context, md->streams[i].zrtphash, 128);
-				/* Turn on the flag to use it if ZRTP is set */
-				md->streams[i].haveZrtpHash = (getParams()->getMediaEncryption() == LinphoneMediaEncryptionZRTP);
-			} else
-				md->streams[i].haveZrtpHash = 0;
-		}
-	}
-}
 
 void MediaSessionPrivate::setupImEncryptionEngineParameters (SalMediaDescription *md) {
 	L_Q();
@@ -1839,11 +1336,9 @@ void MediaSessionPrivate::setupEncryptionKeys (SalMediaDescription *md) {
 	L_Q();
 	SalMediaDescription *oldMd = localDesc;
 	bool keepSrtpKeys = !!lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "sip", "keep_srtp_keys", 1);
-	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&md->streams[i]))
-			continue;
+	for (int i = 0; i < md->nb_streams; i++) {
 		if (sal_stream_description_has_srtp(&md->streams[i])) {
-			if (keepSrtpKeys && oldMd && sal_stream_description_active(&oldMd->streams[i]) && sal_stream_description_has_srtp(&oldMd->streams[i])) {
+			if (keepSrtpKeys && oldMd && sal_stream_description_enabled(&oldMd->streams[i]) && sal_stream_description_has_srtp(&oldMd->streams[i])) {
 				lInfo() << "Keeping same crypto keys";
 				for (int j = 0; j < SAL_CRYPTO_ALGO_MAX; j++) {
 					memcpy(&md->streams[i].crypto[j], &oldMd->streams[i].crypto[j], sizeof(SalSrtpCryptoAlgo));
@@ -1866,2141 +1361,216 @@ void MediaSessionPrivate::transferAlreadyAssignedPayloadTypes (SalMediaDescripti
 }
 
 void MediaSessionPrivate::updateLocalMediaDescriptionFromIce () {
-	iceAgent->updateLocalMediaDescriptionFromIce(localDesc);
-	iceAgent->updateIceStateInCallStats();
+	OfferAnswerContext ctx;
+	ctx.localMediaDescription = localDesc;
+	ctx.remoteMediaDescription = op ? op->getRemoteMediaDescription() : nullptr;
+	getStreamsGroup().fillLocalMediaDescription(ctx);
+	if (op) op->setLocalMediaDescription(localDesc);
 }
 
-// -----------------------------------------------------------------------------
 
-SalMulticastRole MediaSessionPrivate::getMulticastRole (SalStreamType type) {
+void MediaSessionPrivate::performMutualAuthentication(){
 	L_Q();
-	SalMulticastRole multicastRole = SalMulticastInactive;
-	if (op) {
-		SalStreamDescription *streamDesc = nullptr;
-		SalMediaDescription *remoteDesc = op->getRemoteMediaDescription();
-		if (!localDesc && !remoteDesc && (direction == LinphoneCallOutgoing)) {
-			/* Well using call dir */
-			if (((type == SalAudio) && getParams()->audioMulticastEnabled())
-				|| ((type == SalVideo) && getParams()->videoMulticastEnabled()))
-				multicastRole = SalMulticastSender;
-		} else if (localDesc && (!remoteDesc || op->isOfferer())) {
-			streamDesc = sal_media_description_find_best_stream(localDesc, type);
-		} else if (!op->isOfferer() && remoteDesc) {
-			streamDesc = sal_media_description_find_best_stream(remoteDesc, type);
-		}
-
-		if (streamDesc)
-			multicastRole = streamDesc->multicast_role;
+	
+	// Perform mutual authentication if instant messaging encryption is enabled
+	auto encryptionEngine = q->getCore()->getEncryptionEngine();
+	// Is call direction really relevant ? might be linked to offerer/answerer rather than call direction ?
+	Stream *stream = mainAudioStreamIndex != -1 ? getStreamsGroup().getStream(mainAudioStreamIndex) : nullptr;
+	MS2AudioStream *ms2a = dynamic_cast<MS2AudioStream*>(stream);
+	if (encryptionEngine && ms2a && ms2a->getZrtpContext()) {
+		encryptionEngine->mutualAuthentication(
+							ms2a->getZrtpContext(),
+							op->getLocalMediaDescription(),
+							op->getRemoteMediaDescription(),
+							q->getDirection()
+							);
 	}
-	lInfo() << "CallSession [" << q << "], stream type [" << sal_stream_type_to_string(type) << "], multicast role is ["
-		<< sal_multicast_role_to_string(multicastRole) << "]";
-	return multicastRole;
 }
 
-void MediaSessionPrivate::joinMulticastGroup (int streamIndex, MediaStream *ms) {
-	L_Q();
-	if (!mediaPorts[streamIndex].multicastIp.empty())
-		media_stream_join_multicast_group(ms, mediaPorts[streamIndex].multicastIp.c_str());
-	else
-		lError() << "Cannot join multicast group if multicast ip is not set for call [" << q << "]";
-}
-
-// -----------------------------------------------------------------------------
 
-void MediaSessionPrivate::setDtlsFingerprint (MSMediaStreamSessions *sessions, const SalStreamDescription *sd, const SalStreamDescription *remote) {
-	if (sal_stream_description_has_dtls(sd)) {
-		if (sd->dtls_role == SalDtlsRoleInvalid)
-			lWarning() << "Unable to start DTLS engine on stream session [" << sessions << "], Dtls role in resulting media description is invalid";
-		else { /* If DTLS is available at both end points */
-			/* Give the peer certificate fingerprint to dtls context */
-			ms_dtls_srtp_set_peer_fingerprint(sessions->dtls_context, remote->dtls_fingerprint);
-		}
+void MediaSessionPrivate::startDtlsOnAllStreams () {
+	OfferAnswerContext params;
+	params.localMediaDescription = localDesc;
+	params.remoteMediaDescription = op->getRemoteMediaDescription();
+	params.resultMediaDescription = resultDesc;
+	if (params.remoteMediaDescription && params.resultMediaDescription){
+		getStreamsGroup().startDtls(params);
 	}
 }
 
-void MediaSessionPrivate::setDtlsFingerprintOnAudioStream () {
-	SalMediaDescription *remote = op->getRemoteMediaDescription();
-	SalMediaDescription *result = op->getFinalMediaDescription();
-
-	if (!remote || !result) {
-		/* This can happen in some tricky cases (early-media without SDP in the 200). In that case, simply skip DTLS code */
-		return;
-	}
 
-	if (audioStream && (media_stream_get_state(&audioStream->ms) == MSStreamStarted))
-		setDtlsFingerprint(&audioStream->ms.sessions, sal_media_description_find_best_stream(result, SalAudio), sal_media_description_find_best_stream(remote, SalAudio));
+/*
+ * Frees the media resources of the call.
+ * This has to be done at the earliest, unlike signaling resources that sometimes need to be kept a bit more longer.
+ * It is called by setTerminated() (for termination of calls signaled to the application), or directly by the destructor of the session
+ * if it was never notified to the application.
+ */
+void MediaSessionPrivate::freeResources () {
+	getStreamsGroup().finish();
 }
 
-void MediaSessionPrivate::setDtlsFingerprintOnVideoStream () {
-#if VIDEO_ENABLED
-	SalMediaDescription *remote = op->getRemoteMediaDescription();
-	SalMediaDescription *result = op->getFinalMediaDescription();
-
-	if (!remote || !result) {
-		/* This can happen in some tricky cases (early-media without SDP in the 200). In that case, simply skip DTLS code */
-		return;
+/* 
+ * IceServiceListener implementation
+ */
+void MediaSessionPrivate::onGatheringFinished(IceService &service){
+	L_Q();
+	updateLocalMediaDescriptionFromIce();
+	switch (state) {
+		case CallSession::State::IncomingReceived:
+		case CallSession::State::IncomingEarlyMedia:
+			if (callAcceptanceDefered) startAccept();
+			break;
+		case CallSession::State::Updating:
+			startUpdate();
+			break;
+		case CallSession::State::UpdatedByRemote:
+			startAcceptUpdate(prevState, Utils::toString(prevState));
+			break;
+		case CallSession::State::OutgoingInit:
+			q->startInvite(nullptr, "");
+			break;
+		case CallSession::State::Idle:
+			deferIncomingNotification = false;
+			startIncomingNotification();
+			break;
+		default:
+			break;
 	}
-	
-	if (videoStream && (media_stream_get_state(&videoStream->ms) == MSStreamStarted))
-		setDtlsFingerprint(&videoStream->ms.sessions, sal_media_description_find_best_stream(result, SalVideo), sal_media_description_find_best_stream(remote, SalVideo));
-#endif
 }
 
-void MediaSessionPrivate::setDtlsFingerprintOnTextStream () {
-	SalMediaDescription *remote = op->getRemoteMediaDescription();
-	SalMediaDescription *result = op->getFinalMediaDescription();
-
-	if (!remote || !result) {
-		/* This can happen in some tricky cases (early-media without SDP in the 200). In that case, simply skip DTLS code */
-		return;
+void MediaSessionPrivate::onIceCompleted(IceService &service){
+	L_Q();
+	/* The ICE session has succeeded, so perform a call update */
+	if (!getStreamsGroup().getIceService().hasCompletedCheckList()) return;
+	if (getStreamsGroup().getIceService().isControlling() && getParams()->getPrivate()->getUpdateCallWhenIceCompleted()) {
+		if (state == CallSession::State::StreamsRunning){
+			MediaSessionParams newParams(*getParams());
+			newParams.getPrivate()->setInternalCallUpdate(true);
+			q->update(&newParams);
+		}else{
+			lWarning() << "Cannot send reINVITE for ICE during state " << state;
+		}
 	}
-
-	if (textStream && (media_stream_get_state(&textStream->ms) == MSStreamStarted))
-		setDtlsFingerprint(&textStream->ms.sessions, sal_media_description_find_best_stream(result, SalText), sal_media_description_find_best_stream(remote, SalText));
-}
-
-void MediaSessionPrivate::setDtlsFingerprintOnAllStreams () {
-	setDtlsFingerprintOnAudioStream();
-	setDtlsFingerprintOnVideoStream();
-	setDtlsFingerprintOnTextStream();
+	startDtlsOnAllStreams();
 }
 
-void MediaSessionPrivate::setupDtlsParams (MediaStream *ms) {
-	L_Q();
-	if (getParams()->getMediaEncryption() == LinphoneMediaEncryptionDTLS) {
-		MSDtlsSrtpParams dtlsParams = { 0 };
-		
-		/* TODO : search for a certificate with CNAME=sip uri(retrieved from variable me) or default : linphone-dtls-default-identity */
-		/* This will parse the directory to find a matching fingerprint or generate it if not found */
-		/* returned string must be freed */
-		char *certificate = nullptr;
-		char *key = nullptr;
-		char *fingerprint = nullptr;
-
-		sal_certificates_chain_parse_directory(&certificate, &key, &fingerprint,
-			linphone_core_get_user_certificates_path(q->getCore()->getCCore()), "linphone-dtls-default-identity", SAL_CERTIFICATE_RAW_FORMAT_PEM, true, true);
-		if (fingerprint) {
-			dtlsCertificateFingerprint = fingerprint;
-			ms_free(fingerprint);
-		}
-		if (key && certificate) {
-			dtlsParams.pem_certificate = certificate;
-			dtlsParams.pem_pkey = key;
-			dtlsParams.role = MSDtlsSrtpRoleUnset; /* Default is unset, then check if we have a result SalMediaDescription */
-			media_stream_enable_dtls(ms, &dtlsParams);
-			ms_free(certificate);
-			ms_free(key);
-		} else {
-			lError() << "Unable to retrieve or generate DTLS certificate and key - DTLS disabled";
-			/* TODO : check if encryption forced, if yes, stop call */
+void MediaSessionPrivate::onLosingPairsCompleted(IceService &service){
+	if (state == CallSession::State::UpdatedByRemote) {
+		if (incomingIceReinvitePending){
+			lInfo() << "Finished adding losing pairs, ICE re-INVITE can be answered.";
+			startAcceptUpdate(prevState, Utils::toString(prevState));
+			incomingIceReinvitePending = false;
 		}
 	}
 }
 
-void MediaSessionPrivate::setZrtpCryptoTypesParameters (MSZrtpParams *params) {
+void MediaSessionPrivate::onIceRestartNeeded(IceService & service){
 	L_Q();
-	if (!params)
-		return;
-
-	const MSCryptoSuite *srtpSuites = linphone_core_get_srtp_crypto_suites(q->getCore()->getCCore());
-	if (srtpSuites) {
-		for(int i = 0; (srtpSuites[i] != MS_CRYPTO_SUITE_INVALID) && (i < SAL_CRYPTO_ALGO_MAX) && (i < MS_MAX_ZRTP_CRYPTO_TYPES); i++) {
-			switch (srtpSuites[i]) {
-				case MS_AES_128_SHA1_32:
-					params->ciphers[params->ciphersCount++] = MS_ZRTP_CIPHER_AES1;
-					params->authTags[params->authTagsCount++] = MS_ZRTP_AUTHTAG_HS32;
-					break;
-				case MS_AES_128_NO_AUTH:
-					params->ciphers[params->ciphersCount++] = MS_ZRTP_CIPHER_AES1;
-					break;
-				case MS_NO_CIPHER_SHA1_80:
-					params->authTags[params->authTagsCount++] = MS_ZRTP_AUTHTAG_HS80;
-					break;
-				case MS_AES_128_SHA1_80:
-					params->ciphers[params->ciphersCount++] = MS_ZRTP_CIPHER_AES1;
-					params->authTags[params->authTagsCount++] = MS_ZRTP_AUTHTAG_HS80;
-					break;
-				case MS_AES_CM_256_SHA1_80:
-					lWarning() << "Deprecated crypto suite MS_AES_CM_256_SHA1_80, use MS_AES_256_SHA1_80 instead";
-					BCTBX_NO_BREAK;
-				case MS_AES_256_SHA1_80:
-					params->ciphers[params->ciphersCount++] = MS_ZRTP_CIPHER_AES3;
-					params->authTags[params->authTagsCount++] = MS_ZRTP_AUTHTAG_HS80;
-					break;
-				case MS_AES_256_SHA1_32:
-					params->ciphers[params->ciphersCount++] = MS_ZRTP_CIPHER_AES3;
-					params->authTags[params->authTagsCount++] = MS_ZRTP_AUTHTAG_HS32;
-					break;
-				case MS_CRYPTO_SUITE_INVALID:
-					break;
-			}
-		}
-	}
+	getStreamsGroup().getIceService().restartSession(IR_Controlling);
+	MediaSessionParams newParams(*getParams());
+	q->update(&newParams);
+}
 
-	/* linphone_core_get_srtp_crypto_suites is used to determine sensible defaults; here each can be overridden */
-	MsZrtpCryptoTypesCount ciphersCount = linphone_core_get_zrtp_cipher_suites(q->getCore()->getCCore(), params->ciphers); /* if not present in config file, params->ciphers is not modified */
-	if (ciphersCount != 0) /* Use zrtp_cipher_suites config only when present, keep config from srtp_crypto_suite otherwise */
-		params->ciphersCount = ciphersCount;
-	params->hashesCount = linphone_core_get_zrtp_hash_suites(q->getCore()->getCCore(), params->hashes);
-	MsZrtpCryptoTypesCount authTagsCount = linphone_core_get_zrtp_auth_suites(q->getCore()->getCCore(), params->authTags); /* If not present in config file, params->authTags is not modified */
-	if (authTagsCount != 0)
-		params->authTagsCount = authTagsCount; /* Use zrtp_auth_suites config only when present, keep config from srtp_crypto_suite otherwise */
-	params->sasTypesCount = linphone_core_get_zrtp_sas_suites(q->getCore()->getCCore(), params->sasTypes);
-	params->keyAgreementsCount = linphone_core_get_zrtp_key_agreement_suites(q->getCore()->getCCore(), params->keyAgreements);
-	
-	bool haveRemoteZrtpHash = false;
-	if (op && op->getRemoteMediaDescription()) {
-		const SalStreamDescription *remoteStream = sal_media_description_find_best_stream(op->getRemoteMediaDescription(), SalAudio);
-		if (remoteStream) {
-			haveRemoteZrtpHash = remoteStream->haveZrtpHash;
-		}
-	}
-	
-	params->autoStart =  (getParams()->getMediaEncryption() != LinphoneMediaEncryptionZRTP) && (haveRemoteZrtpHash == false) ;
+void MediaSessionPrivate::tryEarlyMediaForking (SalMediaDescription *md) {
+	OfferAnswerContext ctx;
+	ctx.localMediaDescription = localDesc;
+	ctx.remoteMediaDescription = md;
+	ctx.resultMediaDescription = resultDesc;
+	lInfo() << "Early media response received from another branch, checking if media can be forked to this new destination";
+	getStreamsGroup().tryEarlyMediaForking(ctx);
 }
 
-void MediaSessionPrivate::startDtls (MSMediaStreamSessions *sessions, const SalStreamDescription *sd, const SalStreamDescription *remote) {
+void MediaSessionPrivate::updateStreamFrozenPayloads (SalStreamDescription *resultDesc, SalStreamDescription *localStreamDesc) {
 	L_Q();
-	
-	if (sal_stream_description_has_dtls(sd)) {
-		if (sd->dtls_role == SalDtlsRoleInvalid)
-			lWarning() << "Unable to start DTLS engine on stream session [" << sessions << "], Dtls role in resulting media description is invalid";
-		else { 
-			/* Workaround for buggy openssl versions that send DTLS packets bigger than the MTU. We need to increase the recv buf size of the RtpSession.*/
-			int recv_buf_size = lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()),"rtp", "dtls_recv_buf_size", 5000);
-			rtp_session_set_recv_buf_size(sessions->rtp_session, recv_buf_size);
-			
-			/* If DTLS is available at both end points */
-			/* Give the peer certificate fingerprint to dtls context */
-			ms_dtls_srtp_set_peer_fingerprint(sessions->dtls_context, remote->dtls_fingerprint);
-			ms_dtls_srtp_set_role(sessions->dtls_context, (sd->dtls_role == SalDtlsRoleIsClient) ? MSDtlsSrtpRoleIsClient : MSDtlsSrtpRoleIsServer); /* Set the role to client */
-			ms_dtls_srtp_start(sessions->dtls_context); /* Then start the engine, it will send the DTLS client Hello */
+	for (bctbx_list_t *elem = resultDesc->payloads; elem != nullptr; elem = bctbx_list_next(elem)) {
+		OrtpPayloadType *pt = reinterpret_cast<OrtpPayloadType *>(bctbx_list_get_data(elem));
+		if (PayloadTypeHandler::isPayloadTypeNumberAvailable(localStreamDesc->already_assigned_payloads, payload_type_get_number(pt), nullptr)) {
+			/* New codec, needs to be added to the list */
+			localStreamDesc->already_assigned_payloads = bctbx_list_append(localStreamDesc->already_assigned_payloads, payload_type_clone(pt));
+			lInfo() << "CallSession[" << q << "] : payload type " << payload_type_get_number(pt) << " " << pt->mime_type << "/" << pt->clock_rate
+				<< " fmtp=" << L_C_TO_STRING(pt->recv_fmtp) << " added to frozen list";
 		}
 	}
 }
 
-void MediaSessionPrivate::startDtlsOnAudioStream () {
-	SalMediaDescription *remote = op->getRemoteMediaDescription();
-	SalMediaDescription *result = op->getFinalMediaDescription();
-
-	if (!remote || !result) {
-		/* This can happen in some tricky cases (early-media without SDP in the 200). In that case, simply skip DTLS code */
-		return;
+void MediaSessionPrivate::updateFrozenPayloads (SalMediaDescription *result) {
+	for (int i = 0; i < result->nb_streams; i++) {
+		updateStreamFrozenPayloads(&result->streams[i], &localDesc->streams[i]);
 	}
-
-	if (audioStream && (media_stream_get_state(&audioStream->ms) == MSStreamStarted))
-		startDtls(&audioStream->ms.sessions, sal_media_description_find_best_stream(result, SalAudio), sal_media_description_find_best_stream(remote, SalAudio));
 }
 
-void MediaSessionPrivate::startDtlsOnVideoStream () {
-#ifdef VIDEO_ENABLED
-	SalMediaDescription *remote = op->getRemoteMediaDescription();
-	SalMediaDescription *result = op->getFinalMediaDescription();
+void MediaSessionPrivate::updateStreams (SalMediaDescription *newMd, CallSession::State targetState) {
+	L_Q();
 
-	if (!remote || !result) {
-		/* This can happen in some tricky cases (early-media without SDP in the 200). In that case, simply skip DTLS code */
-		return;
+	if (state == CallSession::State::Connected || state == CallSession::State::Resuming ||
+		(state == CallSession::State::IncomingEarlyMedia && !linphone_core_get_ring_during_incoming_early_media(q->getCore()->getCCore()))) {
+		q->getCore()->getPrivate()->getToneManager()->goToCall(q->getSharedFromThis());
 	}
 
-	if (videoStream && (media_stream_get_state(&videoStream->ms) == MSStreamStarted))
-		startDtls(&videoStream->ms.sessions, sal_media_description_find_best_stream(result, SalVideo), sal_media_description_find_best_stream(remote, SalVideo));
-#endif
-}
-
-void MediaSessionPrivate::startDtlsOnTextStream () {
-	SalMediaDescription *remote = op->getRemoteMediaDescription();
-	SalMediaDescription *result = op->getFinalMediaDescription();
-
-	if (!remote || !result) {
-		/* This can happen in some tricky cases (early-media without SDP in the 200). In that case, simply skip DTLS code */
+	if (!newMd) {
+		lError() << "updateStreams() called with null media description";
 		return;
 	}
+	
+	updateBiggestDesc(localDesc);
+	sal_media_description_ref(newMd);
+	SalMediaDescription *oldMd = resultDesc;
+	resultDesc = newMd;
+	
+	OfferAnswerContext ctx;
+	ctx.localMediaDescription = localDesc;
+	ctx.remoteMediaDescription = op->getRemoteMediaDescription();
+	ctx.resultMediaDescription = resultDesc;
+	getStreamsGroup().render(ctx, targetState);
 
-	if (textStream && (media_stream_get_state(&textStream->ms) == MSStreamStarted))
-		startDtls(&textStream->ms.sessions, sal_media_description_find_best_stream(result, SalText), sal_media_description_find_best_stream(remote, SalText));
-}
-
-
-//might be the same interface as startDtls if audio_stream_start_zrtp is replaced by audio_streamsessions_start_zrtp
-void MediaSessionPrivate::startZrtpPrimaryChannel(const SalStreamDescription *remote) {
-	if (remote->type != SalAudio) {
-		lError() << "Cannot start primary zrtp channel for stream type ["
-		<< sal_stream_type_to_string(remote->type) << "]";
-		return;
-	}
-	audio_stream_start_zrtp(audioStream);
-	if (remote->haveZrtpHash == 1) {
-		int retval = ms_zrtp_setPeerHelloHash(audioStream->ms.sessions.zrtp_context, (uint8_t *)remote->zrtphash, strlen((const char *)(remote->zrtphash)));
-		if (retval != 0)
-			lError() << "ZRTP hash mismatch 0x" << hex << retval;
+	if ((state == CallSession::State::Pausing) && pausedByApp && (q->getCore()->getCallCount() == 1)) {
+		q->getCore()->getPrivate()->getToneManager()->startNamedTone(q->getSharedFromThis(), LinphoneToneCallOnHold);
 	}
-	return;
-}
 
-void MediaSessionPrivate::startDtlsOnAllStreams () {
-	startDtlsOnAudioStream();
-	startDtlsOnVideoStream();
-	startDtlsOnTextStream();
+	updateFrozenPayloads(newMd);
+	upBandwidth = linphone_core_get_upload_bandwidth(q->getCore()->getCCore());
+
+	if (oldMd)
+		sal_media_description_unref(oldMd);
 }
 
-void MediaSessionPrivate::updateStreamCryptoParameters (SalStreamDescription *oldStream, SalStreamDescription *newStream) {
-	if (!oldStream || !newStream || oldStream->type != newStream->type)
-		return;
+// -----------------------------------------------------------------------------
 
-	const SalStreamDescription *localStreamDesc = sal_media_description_find_secure_stream_of_type(localDesc, newStream->type);
-	if (newStream->type == SalAudio) {
-		if (audioStream && localStreamDesc) {
-			updateCryptoParameters(localStreamDesc, oldStream, newStream, &audioStream->ms);
-			startDtlsOnAudioStream();
-		}
-	}
-#ifdef VIDEO_ENABLED
-	else if (newStream->type == SalVideo) {
-		if (videoStream && localStreamDesc) {
-			updateCryptoParameters(localStreamDesc, oldStream, newStream, &videoStream->ms);
-			startDtlsOnVideoStream();
-		}
-	}
-#endif
-	else if (newStream->type == SalText) {
-		if (textStream && localStreamDesc) {
-			updateCryptoParameters(localStreamDesc, oldStream, newStream, &textStream->ms);
-			startDtlsOnTextStream();
-		}
-	}
+bool MediaSessionPrivate::allStreamsAvpfEnabled () const {
+	return getStreamsGroup().avpfEnabled();
 }
 
-void MediaSessionPrivate::updateStreamsCryptoParameters (SalMediaDescription *oldMd, SalMediaDescription *newMd) {
-	const SalStreamDescription *localStreamDesc = sal_media_description_find_secure_stream_of_type(localDesc, SalAudio);
-	SalStreamDescription *oldStream = sal_media_description_find_secure_stream_of_type(oldMd, SalAudio);
-	SalStreamDescription *newStream = sal_media_description_find_secure_stream_of_type(newMd, SalAudio);
-	if (audioStream && localStreamDesc && oldStream && newStream)
-		updateCryptoParameters(localStreamDesc, oldStream, newStream, &audioStream->ms);
-#ifdef VIDEO_ENABLED
-	localStreamDesc = sal_media_description_find_secure_stream_of_type(localDesc, SalVideo);
-	oldStream = sal_media_description_find_secure_stream_of_type(oldMd, SalVideo);
-	newStream = sal_media_description_find_secure_stream_of_type(newMd, SalVideo);
-	if (videoStream && localStreamDesc && oldStream && newStream)
-		updateCryptoParameters(localStreamDesc, oldStream, newStream, &videoStream->ms);
-#endif
-	localStreamDesc = sal_media_description_find_secure_stream_of_type(localDesc, SalText);
-	oldStream = sal_media_description_find_secure_stream_of_type(oldMd, SalText);
-	newStream = sal_media_description_find_secure_stream_of_type(newMd, SalText);
-	if (textStream && localStreamDesc && oldStream && newStream)
-		updateCryptoParameters(localStreamDesc, oldStream, newStream, &textStream->ms);
-	startDtlsOnAllStreams();
+bool MediaSessionPrivate::allStreamsEncrypted () const {
+	return getStreamsGroup().allStreamsEncrypted();
 }
 
-bool MediaSessionPrivate::updateCryptoParameters (const SalStreamDescription *localStreamDesc, SalStreamDescription *oldStream, SalStreamDescription *newStream, MediaStream *ms) {
-	int cryptoIdx = Sal::findCryptoIndexFromTag(localStreamDesc->crypto, static_cast<unsigned char>(newStream->crypto_local_tag));
-	if (cryptoIdx >= 0) {
-		if (localDescChanged & SAL_MEDIA_DESCRIPTION_CRYPTO_KEYS_CHANGED)
-			ms_media_stream_sessions_set_srtp_send_key_b64(&ms->sessions, newStream->crypto[0].algo, localStreamDesc->crypto[cryptoIdx].master_key);
-		if (strcmp(oldStream->crypto[0].master_key, newStream->crypto[0].master_key) != 0)
-			ms_media_stream_sessions_set_srtp_recv_key_b64(&ms->sessions, newStream->crypto[0].algo, newStream->crypto[0].master_key);
-		return true;
-	} else
-		lWarning() << "Failed to find local crypto algo with tag: " << newStream->crypto_local_tag;
-	return false;
+bool MediaSessionPrivate::atLeastOneStreamStarted () const {
+	return getStreamsGroup().isStarted();
 }
 
-// -----------------------------------------------------------------------------
+uint16_t MediaSessionPrivate::getAvpfRrInterval () const {
+	return (uint16_t)getStreamsGroup().getAvpfRrInterval();
+}
 
-int MediaSessionPrivate::getIdealAudioBandwidth (const SalMediaDescription *md, const SalStreamDescription *desc) {
-	L_Q();
-	int remoteBandwidth = 0;
-	if (desc->bandwidth > 0)
-		remoteBandwidth = desc->bandwidth;
-	else if (md->bandwidth > 0) {
-		/* Case where b=AS is given globally, not per stream */
-		remoteBandwidth = md->bandwidth;
-	}
-	int uploadBandwidth = 0;
-	bool forced = false;
-	if (getParams()->getPrivate()->getUpBandwidth() > 0) {
-		forced = true;
-		uploadBandwidth = getParams()->getPrivate()->getUpBandwidth();
-	} else
-		uploadBandwidth = linphone_core_get_upload_bandwidth(q->getCore()->getCCore());
-	uploadBandwidth = PayloadTypeHandler::getMinBandwidth(uploadBandwidth, remoteBandwidth);
-	if (!linphone_core_media_description_contains_video_stream(md) || forced)
-		return uploadBandwidth;
-	if (PayloadTypeHandler::bandwidthIsGreater(uploadBandwidth, 512))
-		uploadBandwidth = 100;
-	else if (PayloadTypeHandler::bandwidthIsGreater(uploadBandwidth, 256))
-		uploadBandwidth = 64;
-	else if (PayloadTypeHandler::bandwidthIsGreater(uploadBandwidth, 128))
-		uploadBandwidth = 40;
-	else if (PayloadTypeHandler::bandwidthIsGreater(uploadBandwidth, 0))
-		uploadBandwidth = 24;
-	return uploadBandwidth;
-}
-
-int MediaSessionPrivate::getVideoBandwidth (const SalMediaDescription *md, const SalStreamDescription *desc) {
-	L_Q();
-	int remoteBandwidth = 0;
-	if (desc->bandwidth > 0)
-		remoteBandwidth = desc->bandwidth;
-	else if (md->bandwidth > 0) {
-		/* Case where b=AS is given globally, not per stream */
-		remoteBandwidth = PayloadTypeHandler::getRemainingBandwidthForVideo(md->bandwidth, audioBandwidth);
-	}
-	return PayloadTypeHandler::getMinBandwidth(PayloadTypeHandler::getRemainingBandwidthForVideo(linphone_core_get_upload_bandwidth(q->getCore()->getCCore()), audioBandwidth), remoteBandwidth);
+unsigned int MediaSessionPrivate::getNbActiveStreams () const {
+	return (unsigned int)getStreamsGroup().getActiveStreamsCount();
 }
 
-RtpProfile * MediaSessionPrivate::makeProfile (const SalMediaDescription *md, const SalStreamDescription *desc, int *usedPt) {
+bool MediaSessionPrivate::isEncryptionMandatory () const {
 	L_Q();
-	*usedPt = -1;
-	int bandwidth = 0;
-	if (desc->type == SalAudio)
-		bandwidth = getIdealAudioBandwidth(md, desc);
-	else if (desc->type == SalVideo)
-		bandwidth = getVideoBandwidth(md, desc);
-
-	bool first = true;
-	RtpProfile *profile = rtp_profile_new("Call profile");
-	for (const bctbx_list_t *elem = desc->payloads; elem != nullptr; elem = bctbx_list_next(elem)) {
-		OrtpPayloadType *pt = reinterpret_cast<OrtpPayloadType *>(bctbx_list_get_data(elem));
-		/* Make a copy of the payload type, so that we left the ones from the SalStreamDescription unchanged.
-		 * If the SalStreamDescription is freed, this will have no impact on the running streams. */
-		pt = payload_type_clone(pt);
-		int upPtime = 0;
-		if ((pt->flags & PAYLOAD_TYPE_FLAG_CAN_SEND) && first) {
-			/* First codec in list is the selected one */
-			if (desc->type == SalAudio) {
-				updateAllocatedAudioBandwidth(pt, bandwidth);
-				bandwidth = audioBandwidth;
-				upPtime = getParams()->getPrivate()->getUpPtime();
-				if (!upPtime)
-					upPtime = linphone_core_get_upload_ptime(q->getCore()->getCCore());
-			}
-			first = false;
-		}
-		if (*usedPt == -1) {
-			/* Don't select telephone-event as a payload type */
-			if (strcasecmp(pt->mime_type, "telephone-event") != 0)
-				*usedPt = payload_type_get_number(pt);
-		}
-		if (pt->flags & PAYLOAD_TYPE_BITRATE_OVERRIDE) {
-			lInfo() << "Payload type [" << pt->mime_type << "/" << pt->clock_rate << "] has explicit bitrate [" << (pt->normal_bitrate / 1000) << "] kbit/s";
-			pt->normal_bitrate = PayloadTypeHandler::getMinBandwidth(pt->normal_bitrate, bandwidth * 1000);
-		} else
-			pt->normal_bitrate = bandwidth * 1000;
-		if (desc->maxptime > 0) {// follow the same schema for maxptime as for ptime. (I.E add it to fmtp)
-			ostringstream os;
-			os << "maxptime=" << desc->maxptime;
-			payload_type_append_send_fmtp(pt, os.str().c_str());
-		}
-		if (desc->ptime > 0)
-			upPtime = desc->ptime;
-		if (upPtime > 0) {
-			ostringstream os;
-			os << "ptime=" << upPtime;
-			payload_type_append_send_fmtp(pt, os.str().c_str());
-		}
-		int number = payload_type_get_number(pt);
-		if (rtp_profile_get_payload(profile, number))
-			lWarning() << "A payload type with number " << number << " already exists in profile!";
-		else
-			rtp_profile_set_payload(profile, number, pt);
+	if (getParams()->getMediaEncryption() == LinphoneMediaEncryptionDTLS) {
+		lInfo() << "Forced encryption mandatory on CallSession [" << q << "] due to SRTP-DTLS";
+		return true;
 	}
-	return profile;
-}
-
-void MediaSessionPrivate::unsetRtpProfile (int streamIndex) {
-	if (sessions[streamIndex].rtp_session)
-		rtp_session_set_profile(sessions[streamIndex].rtp_session, &av_profile);
-}
-
-void MediaSessionPrivate::updateAllocatedAudioBandwidth (const PayloadType *pt, int maxbw) {
-	L_Q();
-	audioBandwidth = PayloadTypeHandler::getAudioPayloadTypeBandwidth(pt, maxbw);
-	lInfo() << "Audio bandwidth for CallSession [" << q << "] is " << audioBandwidth;
-}
-
-// -----------------------------------------------------------------------------
-
-void MediaSessionPrivate::applyJitterBufferParams (RtpSession *session, LinphoneStreamType type) {
-	L_Q();
-	LinphoneConfig *config = linphone_core_get_config(q->getCore()->getCCore());
-	JBParameters params;
-	rtp_session_get_jitter_buffer_params(session, &params);
-	params.min_size = lp_config_get_int(config, "rtp", "jitter_buffer_min_size", 40);
-	params.max_size = lp_config_get_int(config, "rtp", "jitter_buffer_max_size", 500);
-	params.max_packets = params.max_size * 200 / 1000; /* Allow 200 packet per seconds, quite large */
-	const char *algo = lp_config_get_string(config, "rtp", "jitter_buffer_algorithm", "rls");
-	params.buffer_algorithm = jitterBufferNameToAlgo(algo ? algo : "");
-	params.refresh_ms = lp_config_get_int(config, "rtp", "jitter_buffer_refresh_period", 5000);
-	params.ramp_refresh_ms = lp_config_get_int(config, "rtp", "jitter_buffer_ramp_refresh_period", 5000);
-	params.ramp_step_ms = lp_config_get_int(config, "rtp", "jitter_buffer_ramp_step", 20);
-	params.ramp_threshold = lp_config_get_int(config, "rtp", "jitter_buffer_ramp_threshold", 70);
-
-	switch (type) {
-		case LinphoneStreamTypeAudio:
-		case LinphoneStreamTypeText: /* Let's use the same params for text as for audio */
-			params.nom_size = linphone_core_get_audio_jittcomp(q->getCore()->getCCore());
-			params.adaptive = linphone_core_audio_adaptive_jittcomp_enabled(q->getCore()->getCCore());
-			break;
-		case LinphoneStreamTypeVideo:
-			params.nom_size = linphone_core_get_video_jittcomp(q->getCore()->getCCore());
-			params.adaptive = linphone_core_video_adaptive_jittcomp_enabled(q->getCore()->getCCore());
-			break;
-		case LinphoneStreamTypeUnknown:
-			lError() << "applyJitterBufferParams: should not happen";
-			break;
-	}
-	params.enabled = params.nom_size > 0;
-	if (params.enabled) {
-		if (params.min_size > params.nom_size)
-			params.min_size = params.nom_size;
-		if (params.max_size < params.nom_size)
-			params.max_size = params.nom_size;
-	}
-	rtp_session_set_jitter_buffer_params(session, &params);
-}
-
-void MediaSessionPrivate::clearEarlyMediaDestination (MediaStream *ms) {
-	L_Q();
-	RtpSession *session = ms->sessions.rtp_session;
-	rtp_session_clear_aux_remote_addr(session);
-	/* Restore symmetric rtp if ICE is not used */
-	if (!iceAgent->hasSession())
-		rtp_session_set_symmetric_rtp(session, linphone_core_symmetric_rtp_enabled(q->getCore()->getCCore()));
-}
-
-void MediaSessionPrivate::clearEarlyMediaDestinations () {
-	if (audioStream)
-		clearEarlyMediaDestination(&audioStream->ms);
-	if (videoStream)
-		clearEarlyMediaDestination(&videoStream->ms);
-}
-
-void MediaSessionPrivate::configureAdaptiveRateControl (MediaStream *ms, const OrtpPayloadType *pt, bool videoWillBeUsed) {
-	L_Q();
-	bool enabled = !!linphone_core_adaptive_rate_control_enabled(q->getCore()->getCCore());
-	if (!enabled) {
-		media_stream_enable_adaptive_bitrate_control(ms, false);
-		return;
-	}
-	bool isAdvanced = true;
-	string algo = linphone_core_get_adaptive_rate_algorithm(q->getCore()->getCCore());
-	if (algo == "basic")
-		isAdvanced = false;
-	else if (algo == "advanced")
-		isAdvanced = true;
-	if (isAdvanced) {
-		/* We can't use media_stream_avpf_enabled() here because the active PayloadType is not set yet in the MediaStream */
-		if (!pt || !(pt->flags & PAYLOAD_TYPE_RTCP_FEEDBACK_ENABLED)) {
-			lWarning() << "CallSession [" << q << "] - advanced adaptive rate control requested but avpf is not activated in this stream. Reverting to basic rate control instead";
-			isAdvanced = false;
-		} else
-			lInfo() << "CallSession [" << q << "] - setting up advanced rate control";
-	}
-	if (isAdvanced) {
-		ms_bandwidth_controller_add_stream(q->getCore()->getCCore()->bw_controller, ms);
-		media_stream_enable_adaptive_bitrate_control(ms, false);
-	} else {
-		media_stream_set_adaptive_bitrate_algorithm(ms, MSQosAnalyzerAlgorithmSimple);
-		if ((ms->type == MSAudio) && videoWillBeUsed) {
-			/* If this is an audio stream but video is going to be used, there is no need to perform
-			 * basic rate control on the audio stream, just the video stream. */
-			enabled = false;
-		}
-		media_stream_enable_adaptive_bitrate_control(ms, enabled);
-	}
-}
-
-void MediaSessionPrivate::configureRtpSessionForRtcpFb (const SalStreamDescription *stream) {
-	RtpSession *session = nullptr;
-	if (stream->type == SalAudio)
-		session = audioStream->ms.sessions.rtp_session;
-	else if (stream->type == SalVideo)
-		session = videoStream->ms.sessions.rtp_session;
-	else
-		return; /* Do nothing for streams that are not audio or video */
-	if (stream->rtcp_fb.generic_nack_enabled)
-		rtp_session_enable_avpf_feature(session, ORTP_AVPF_FEATURE_GENERIC_NACK, true);
-	else
-		rtp_session_enable_avpf_feature(session, ORTP_AVPF_FEATURE_GENERIC_NACK, false);
-	if (stream->rtcp_fb.tmmbr_enabled)
-		rtp_session_enable_avpf_feature(session, ORTP_AVPF_FEATURE_TMMBR, true);
-	else
-		rtp_session_enable_avpf_feature(session, ORTP_AVPF_FEATURE_TMMBR, false);
-}
-
-void MediaSessionPrivate::configureRtpSessionForRtcpXr (SalStreamType type) {
-	SalMediaDescription *remote = op->getRemoteMediaDescription();
-	if (!remote)
-		return;
-	const SalStreamDescription *localStream = sal_media_description_find_best_stream(localDesc, type);
-	if (!localStream)
-		return;
-	const SalStreamDescription *remoteStream = sal_media_description_find_best_stream(remote, type);
-	if (!remoteStream)
-		return;
-	OrtpRtcpXrConfiguration currentConfig;
-	const OrtpRtcpXrConfiguration *remoteConfig = &remoteStream->rtcp_xr;
-	if (localStream->dir == SalStreamInactive)
-		return;
-	else if (localStream->dir == SalStreamRecvOnly) {
-		/* Use local config for unilateral parameters and remote config for collaborative parameters */
-		memcpy(&currentConfig, &localStream->rtcp_xr, sizeof(currentConfig));
-		currentConfig.rcvr_rtt_mode = remoteConfig->rcvr_rtt_mode;
-		currentConfig.rcvr_rtt_max_size = remoteConfig->rcvr_rtt_max_size;
-	} else
-		memcpy(&currentConfig, remoteConfig, sizeof(currentConfig));
-	RtpSession *session = nullptr;
-	if (type == SalAudio) {
-		session = audioStream->ms.sessions.rtp_session;
-	} else if (type == SalVideo) {
-		session = videoStream->ms.sessions.rtp_session;
-	} else if (type == SalText) {
-		session = textStream->ms.sessions.rtp_session;
-	}
-	rtp_session_configure_rtcp_xr(session, &currentConfig);
-}
-
-RtpSession * MediaSessionPrivate::createAudioRtpIoSession () {
-	L_Q();
-	LinphoneConfig *config = linphone_core_get_config(q->getCore()->getCCore());
-	const char *rtpmap = lp_config_get_string(config, "sound", "rtp_map", "pcmu/8000/1");
-	OrtpPayloadType *pt = rtp_profile_get_payload_from_rtpmap(audioProfile, rtpmap);
-	if (!pt)
-		return nullptr;
-	rtpIoAudioProfile = rtp_profile_new("RTP IO audio profile");
-	int ptnum = lp_config_get_int(config, "sound", "rtp_ptnum", 0);
-	rtp_profile_set_payload(rtpIoAudioProfile, ptnum, payload_type_clone(pt));
-	const char *localIp = lp_config_get_string(config, "sound", "rtp_local_addr", "127.0.0.1");
-	int localPort = lp_config_get_int(config, "sound", "rtp_local_port", 17076);
-	RtpSession *rtpSession = ms_create_duplex_rtp_session(localIp, localPort, -1, ms_factory_get_mtu(q->getCore()->getCCore()->factory));
-	rtp_session_set_profile(rtpSession, rtpIoAudioProfile);
-	const char *remoteIp = lp_config_get_string(config, "sound", "rtp_remote_addr", "127.0.0.1");
-	int remotePort = lp_config_get_int(config, "sound", "rtp_remote_port", 17078);
-	rtp_session_set_remote_addr_and_port(rtpSession, remoteIp, remotePort, -1);
-	rtp_session_enable_rtcp(rtpSession, false);
-	rtp_session_set_payload_type(rtpSession, ptnum);
-	int jittcomp = lp_config_get_int(config, "sound", "rtp_jittcomp", 0); /* 0 means no jitter buffer */
-	rtp_session_set_jitter_compensation(rtpSession, jittcomp);
-	rtp_session_enable_jitter_buffer(rtpSession, (jittcomp > 0));
-	bool symmetric = !!lp_config_get_int(config, "sound", "rtp_symmetric", 0);
-	rtp_session_set_symmetric_rtp(rtpSession, symmetric);
-	return rtpSession;
-}
-
-RtpSession * MediaSessionPrivate::createVideoRtpIoSession () {
-#ifdef VIDEO_ENABLED
-	L_Q();
-	LinphoneConfig *config = linphone_core_get_config(q->getCore()->getCCore());
-	const char *rtpmap = lp_config_get_string(config, "video", "rtp_map", "vp8/90000/1");
-	OrtpPayloadType *pt = rtp_profile_get_payload_from_rtpmap(videoProfile, rtpmap);
-	if (!pt)
-		return nullptr;
-	rtpIoVideoProfile = rtp_profile_new("RTP IO video profile");
-	int ptnum = lp_config_get_int(config, "video", "rtp_ptnum", 0);
-	rtp_profile_set_payload(rtpIoVideoProfile, ptnum, payload_type_clone(pt));
-	const char *localIp = lp_config_get_string(config, "video", "rtp_local_addr", "127.0.0.1");
-	int localPort = lp_config_get_int(config, "video", "rtp_local_port", 19076);
-	RtpSession *rtpSession = ms_create_duplex_rtp_session(localIp, localPort, -1, ms_factory_get_mtu(q->getCore()->getCCore()->factory));
-	rtp_session_set_profile(rtpSession, rtpIoVideoProfile);
-	const char *remoteIp = lp_config_get_string(config, "video", "rtp_remote_addr", "127.0.0.1");
-	int remotePort = lp_config_get_int(config, "video", "rtp_remote_port", 19078);
-	rtp_session_set_remote_addr_and_port(rtpSession, remoteIp, remotePort, -1);
-	rtp_session_enable_rtcp(rtpSession, false);
-	rtp_session_set_payload_type(rtpSession, ptnum);
-	rtp_session_set_symmetric_rtp(rtpSession, linphone_config_get_bool(config, "video", "rtp_symmetric", FALSE));
-	int jittcomp = lp_config_get_int(config, "video", "rtp_jittcomp", 0); /* 0 means no jitter buffer */
-	rtp_session_set_jitter_compensation(rtpSession, jittcomp);
-	rtp_session_enable_jitter_buffer(rtpSession, (jittcomp > 0));
-	return rtpSession;
-#else
-	return nullptr;
-#endif
-}
-
-/*
- * Frees the media resources of the call.
- * This has to be done at the earliest, unlike signaling resources that sometimes need to be kept a bit more longer.
- * It is called by setTerminated() (for termination of calls signaled to the application), or directly by the destructor of the session
- * if it was never notified to the application.
- */
-void MediaSessionPrivate::freeResources () {
-	stopStreams();
-	iceAgent->deleteSession();
-	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++)
-		ms_media_stream_sessions_uninit(&sessions[i]);
-	_linphone_call_stats_uninit(audioStats);
-	_linphone_call_stats_uninit(videoStats);
-	_linphone_call_stats_uninit(textStats);
-}
-
-void MediaSessionPrivate::handleIceEvents (OrtpEvent *ev) {
-	L_Q();
-	OrtpEventType evt = ortp_event_get_type(ev);
-	OrtpEventData *evd = ortp_event_get_data(ev);
-	if (evt == ORTP_EVENT_ICE_SESSION_PROCESSING_FINISHED) {
-		if (iceAgent->hasCompletedCheckList()) {
-			/* The ICE session has succeeded, so perform a call update */
-			if (iceAgent->isControlling() && q->getCurrentParams()->getPrivate()->getUpdateCallWhenIceCompleted()) {
-				if (state == CallSession::State::StreamsRunning){
-					MediaSessionParams newParams(*getParams());
-					newParams.getPrivate()->setInternalCallUpdate(true);
-					q->update(&newParams);
-				}else{
-					lWarning() << "Cannot send reINVITE for ICE during state " << state;
-				}
-			}else if (!iceAgent->isControlling() && incomingIceReinvitePending){
-				q->acceptUpdate(nullptr);
-				incomingIceReinvitePending = false;
-			}
-			startDtlsOnAllStreams();
-		}
-		iceAgent->updateIceStateInCallStats();
-	} else if (evt == ORTP_EVENT_ICE_GATHERING_FINISHED) {
-		if (!evd->info.ice_processing_successful)
-			lWarning() << "No STUN answer from [" << linphone_nat_policy_get_stun_server(q->getPrivate()->getNatPolicy()) << "], continuing without STUN";
-		iceAgent->gatheringFinished();
-		switch (state) {
-			case CallSession::State::Updating:
-				startUpdate();
-				break;
-			case CallSession::State::UpdatedByRemote:
-				startAcceptUpdate(prevState, Utils::toString(prevState));
-				break;
-			case CallSession::State::OutgoingInit:
-				stopStreamsForIceGathering();
-				if (isReadyForInvite())
-					q->startInvite(nullptr, "");
-				break;
-			case CallSession::State::Idle:
-				stopStreamsForIceGathering();
-				updateLocalMediaDescriptionFromIce();
-				op->setLocalMediaDescription(localDesc);
-				deferIncomingNotification = false;
-				startIncomingNotification();
-				break;
-			default:
-				break;
-		}
-	} else if (evt == ORTP_EVENT_ICE_LOSING_PAIRS_COMPLETED) {
-		if (state == CallSession::State::UpdatedByRemote) {
-			startAcceptUpdate(prevState, Utils::toString(prevState));
-			iceAgent->updateIceStateInCallStats();
-		}
-	} else if (evt == ORTP_EVENT_ICE_RESTART_NEEDED) {
-		iceAgent->restartSession(IR_Controlling);
-		q->update(getCurrentParams());
-	}
-}
-
-void MediaSessionPrivate::handleStreamEvents (int streamIndex) {
-	L_Q();
-
-	MediaStream *ms = getMediaStream(streamIndex);
-	if (ms) {
-		/* Ensure there is no dangling ICE check list */
-		if (!iceAgent->hasSession())
-			media_stream_set_ice_check_list(ms, nullptr);
-		switch(ms->type){
-			case MSAudio:
-				audio_stream_iterate((AudioStream *)ms);
-				break;
-			case MSVideo:
-#ifdef VIDEO_ENABLED
-				video_stream_iterate((VideoStream *)ms);
-#endif
-				break;
-			case MSText:
-				text_stream_iterate((TextStream *)ms);
-				break;
-			default:
-				lError() << "handleStreamEvents(): unsupported stream type";
-				return;
-		}
-	}
-	OrtpEvQueue *evq;
-	OrtpEvent *ev;
-	/* Yes the event queue has to be taken at each iteration, because ice events may perform operations re-creating the streams */
-	while ((evq = getEventQueue(streamIndex)) && (ev = ortp_ev_queue_get(evq))) {
-		LinphoneCallStats *stats = nullptr;
-		if (streamIndex == mainAudioStreamIndex)
-			stats = audioStats;
-		else if (streamIndex == mainVideoStreamIndex)
-			stats = videoStats;
-		else
-			stats = textStats;
-
-		OrtpEventType evt = ortp_event_get_type(ev);
-		OrtpEventData *evd = ortp_event_get_data(ev);
-
-		/*This MUST be done before any call to "linphone_call_stats_fill" since it has ownership over evd->packet*/
-		if (evt == ORTP_EVENT_RTCP_PACKET_RECEIVED) {
-			do {
-				if (evd->packet && rtcp_is_RTPFB(evd->packet)) {
-					if (rtcp_RTPFB_get_type(evd->packet) == RTCP_RTPFB_TMMBR) {
-						listener->onTmmbrReceived(q->getSharedFromThis(), streamIndex, (int)rtcp_RTPFB_tmmbr_get_max_bitrate(evd->packet));
-					}
-				}
-			} while (rtcp_next_packet(evd->packet));
-			rtcp_rewind(evd->packet);
-		}
-
-		/* And yes the MediaStream must be taken at each iteration, because it may have changed due to the handling of events
-		 * in this loop*/
-		ms = getMediaStream(streamIndex);
-		if (ms)
-			linphone_call_stats_fill(stats, ms, ev);
-		notifyStatsUpdated(streamIndex);
-		if (evt == ORTP_EVENT_ZRTP_ENCRYPTION_CHANGED) {
-			if (streamIndex == mainAudioStreamIndex)
-				audioStreamEncryptionChanged(!!evd->info.zrtp_stream_encrypted);
-			else if (streamIndex == mainVideoStreamIndex)
-				propagateEncryptionChanged();
-		} else if (evt == ORTP_EVENT_ZRTP_SAS_READY) {
-			if (streamIndex == mainAudioStreamIndex)
-				audioStreamAuthTokenReady(evd->info.zrtp_info.sas, !!evd->info.zrtp_info.verified);
-		} else if (evt == ORTP_EVENT_DTLS_ENCRYPTION_CHANGED) {
-			if (streamIndex == mainAudioStreamIndex)
-				audioStreamEncryptionChanged(!!evd->info.dtls_stream_encrypted);
-			else if (streamIndex == mainVideoStreamIndex)
-				propagateEncryptionChanged();
-		} else if ((evt == ORTP_EVENT_ICE_SESSION_PROCESSING_FINISHED) || (evt == ORTP_EVENT_ICE_GATHERING_FINISHED)
-			|| (evt == ORTP_EVENT_ICE_LOSING_PAIRS_COMPLETED) || (evt == ORTP_EVENT_ICE_RESTART_NEEDED)) {
-			if (ms)
-				handleIceEvents(ev);
-		} else if (evt == ORTP_EVENT_TELEPHONE_EVENT) {
-			telephoneEventReceived(evd->info.telephone_event);
-		} else if (evt == ORTP_EVENT_NEW_VIDEO_BANDWIDTH_ESTIMATION_AVAILABLE) {
-			lInfo() << "Video bandwidth estimation is " << (int)(evd->info.video_bandwidth_available / 1000.) << " kbit/s";
-			/* If this event happens then it should be a video stream */
-			if (streamIndex == mainVideoStreamIndex)
-				linphone_call_stats_set_estimated_download_bandwidth(stats, (float)(evd->info.video_bandwidth_available*1e-3));
-		}
-		ortp_event_destroy(ev);
-	}
-}
-
-void MediaSessionPrivate::configureRtpSession(RtpSession *session, LinphoneStreamType streamType){
-	L_Q();
-	
-	rtp_session_enable_network_simulation(session, &q->getCore()->getCCore()->net_conf.netsim_params);
-	applyJitterBufferParams(session, streamType);
-	string userAgent = linphone_core_get_user_agent(q->getCore()->getCCore());
-	rtp_session_set_source_description(session, getMe()->getAddress().asString().c_str(), NULL, NULL, NULL, NULL, userAgent.c_str(), NULL);
-	rtp_session_set_symmetric_rtp(session, linphone_core_symmetric_rtp_enabled(q->getCore()->getCCore()));
-	
-	if (streamType == LinphoneStreamTypeVideo){
-		int videoRecvBufSize = lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "video", "recv_buf_size", 0);
-		if (videoRecvBufSize > 0)
-			rtp_session_set_recv_buf_size(videoStream->ms.sessions.rtp_session, videoRecvBufSize);
-	}
-}
-
-
-void MediaSessionPrivate::initializeAudioStream () {
-	L_Q();
-	
-	if (audioStream)
-		return;
-	if (!sessions[mainAudioStreamIndex].rtp_session) {
-		SalMulticastRole multicastRole = getMulticastRole(SalAudio);
-		SalMediaDescription *remoteDesc = nullptr;
-		SalStreamDescription *streamDesc = nullptr;
-		if (op)
-			remoteDesc = op->getRemoteMediaDescription();
-		if (remoteDesc)
-			streamDesc = sal_media_description_find_best_stream(remoteDesc, SalAudio);
-
-		audioStream = audio_stream_new2(q->getCore()->getCCore()->factory, L_STRING_TO_C(getBindIpForStream(mainAudioStreamIndex)),
-			(multicastRole ==  SalMulticastReceiver) ? streamDesc->rtp_port : mediaPorts[mainAudioStreamIndex].rtpPort,
-			(multicastRole ==  SalMulticastReceiver) ? 0 /* Disabled for now */ : mediaPorts[mainAudioStreamIndex].rtcpPort);
-		if (multicastRole == SalMulticastReceiver)
-			joinMulticastGroup(mainAudioStreamIndex, &audioStream->ms);
-		
-		configureRtpSession(audioStream->ms.sessions.rtp_session, LinphoneStreamTypeAudio);
-		setupDtlsParams(&audioStream->ms);
-
-		/* Initialize zrtp even if we didn't explicitely set it, just in case peer offers it */
-		if (linphone_core_media_encryption_supported(q->getCore()->getCCore(), LinphoneMediaEncryptionZRTP)) {
-			LinphoneAddress *peerAddr = (direction == LinphoneCallIncoming) ? log->from : log->to;
-			LinphoneAddress *selfAddr = (direction == LinphoneCallIncoming) ? log->to : log->from;
-			char *peerUri = ms_strdup_printf("%s:%s@%s"	, linphone_address_get_scheme(peerAddr)
-														, linphone_address_get_username(peerAddr)
-														, linphone_address_get_domain(peerAddr));
-			char *selfUri = ms_strdup_printf("%s:%s@%s"	, linphone_address_get_scheme(selfAddr)
-														, linphone_address_get_username(selfAddr)
-														, linphone_address_get_domain(selfAddr));
-
-			MSZrtpParams params;
-			zrtpCacheAccess zrtpCacheInfo = linphone_core_get_zrtp_cache_access(q->getCore()->getCCore());
-
-			memset(&params, 0, sizeof(MSZrtpParams));
-			/* media encryption of current params will be set later when zrtp is activated */
-			params.zidCacheDB = zrtpCacheInfo.db;
-			params.zidCacheDBMutex = zrtpCacheInfo.dbMutex;
-			params.peerUri = peerUri;
-			params.selfUri = selfUri;
-			/* Get key lifespan from config file, default is 0:forever valid */
-			params.limeKeyTimeSpan = bctbx_time_string_to_sec(lp_config_get_string(linphone_core_get_config(q->getCore()->getCCore()), "sip", "lime_key_validity", "0"));
-			setZrtpCryptoTypesParameters(&params);
-			audio_stream_enable_zrtp(audioStream, &params);
-			if (peerUri)
-				ms_free(peerUri);
-			if (selfUri)
-				ms_free(selfUri);
-		}
-
-		media_stream_reclaim_sessions(&audioStream->ms, &sessions[mainAudioStreamIndex]);
-	} else {
-		audioStream = audio_stream_new_with_sessions(q->getCore()->getCCore()->factory, &sessions[mainAudioStreamIndex]);
-	}
-	
-	MSSndCard *playcard = q->getCore()->getCCore()->sound_conf.lsd_card ? q->getCore()->getCCore()->sound_conf.lsd_card : q->getCore()->getCCore()->sound_conf.play_sndcard;
-	if (playcard) {
-		// Set the stream type immediately, as on iOS AudioUnit is instanciated very early because it is 
-		// otherwise too slow to start.
-		ms_snd_card_set_stream_type(playcard, MS_SND_CARD_STREAM_VOICE);
-	}
-
-	if (mediaPorts[mainAudioStreamIndex].rtpPort == -1)
-		setPortConfigFromRtpSession(mainAudioStreamIndex, audioStream->ms.sessions.rtp_session);
-	int dscp = linphone_core_get_audio_dscp(q->getCore()->getCCore());
-	if (dscp != -1)
-		audio_stream_set_dscp(audioStream, dscp);
-	if (linphone_core_echo_limiter_enabled(q->getCore()->getCCore())) {
-		string type = lp_config_get_string(linphone_core_get_config(q->getCore()->getCCore()), "sound", "el_type", "mic");
-		if (type == "mic")
-			audio_stream_enable_echo_limiter(audioStream, ELControlMic);
-		else if (type == "full")
-			audio_stream_enable_echo_limiter(audioStream, ELControlFull);
-	}
-
-	// Equalizer location in the graph: 'mic' = in input graph, otherwise in output graph.
-	// Any other value than mic will default to output graph for compatibility.
-	string location = lp_config_get_string(linphone_core_get_config(q->getCore()->getCCore()), "sound", "eq_location", "hp");
-	audioStream->eq_loc = (location == "mic") ? MSEqualizerMic : MSEqualizerHP;
-	lInfo() << "Equalizer location: " << location;
-
-	audio_stream_enable_gain_control(audioStream, true);
-	if (linphone_core_echo_cancellation_enabled(q->getCore()->getCCore())) {
-		int len = lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "sound", "ec_tail_len", 0);
-		int delay = lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "sound", "ec_delay", 0);
-		int framesize = lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "sound", "ec_framesize", 0);
-		audio_stream_set_echo_canceller_params(audioStream, len, delay, framesize);
-		if (audioStream->ec) {
-			char *statestr=reinterpret_cast<char *>(ms_malloc0(ecStateMaxLen));
-			if (lp_config_relative_file_exists(linphone_core_get_config(q->getCore()->getCCore()), ecStateStore.c_str())
-				&& (lp_config_read_relative_file(linphone_core_get_config(q->getCore()->getCCore()), ecStateStore.c_str(), statestr, ecStateMaxLen) == 0)) {
-				ms_filter_call_method(audioStream->ec, MS_ECHO_CANCELLER_SET_STATE_STRING, statestr);
-			}
-			ms_free(statestr);
-		}
-	}
-	audio_stream_enable_automatic_gain_control(audioStream, linphone_core_agc_enabled(q->getCore()->getCCore()));
-	bool_t enabled = !!lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "sound", "noisegate", 0);
-	audio_stream_enable_noise_gate(audioStream, enabled);
-	audio_stream_set_features(audioStream, linphone_core_get_audio_features(q->getCore()->getCCore()));
-
-	if (q->getCore()->getCCore()->rtptf) {
-		RtpTransport *meta_rtp;
-		RtpTransport *meta_rtcp;
-		rtp_session_get_transports(audioStream->ms.sessions.rtp_session, &meta_rtp, &meta_rtcp);
-		if (!meta_rtp_transport_get_endpoint(meta_rtp)) {
-			lInfo() << "CallSession [" << q << "] using custom audio RTP transport endpoint";
-			meta_rtp_transport_set_endpoint(meta_rtp, q->getCore()->getCCore()->rtptf->audio_rtp_func(q->getCore()->getCCore()->rtptf->audio_rtp_func_data, mediaPorts[mainAudioStreamIndex].rtpPort));
-		}
-		if (!meta_rtp_transport_get_endpoint(meta_rtcp))
-			meta_rtp_transport_set_endpoint(meta_rtcp, q->getCore()->getCCore()->rtptf->audio_rtcp_func(q->getCore()->getCCore()->rtptf->audio_rtcp_func_data, mediaPorts[mainAudioStreamIndex].rtcpPort));
-	}
-
-	audioStreamEvQueue = ortp_ev_queue_new();
-	rtp_session_register_event_queue(audioStream->ms.sessions.rtp_session, audioStreamEvQueue);
-	iceAgent->prepareIceForStream(&audioStream->ms, false);
-}
-
-void MediaSessionPrivate::initializeTextStream () {
-	L_Q();
-	if (textStream)
-		return;
-	if (!sessions[mainTextStreamIndex].rtp_session) {
-		SalMulticastRole multicastRole = getMulticastRole(SalText);
-		SalMediaDescription *remoteDesc = nullptr;
-		SalStreamDescription *streamDesc = nullptr;
-		if (op)
-			remoteDesc = op->getRemoteMediaDescription();
-		if (remoteDesc)
-			streamDesc = sal_media_description_find_best_stream(remoteDesc, SalText);
-
-		textStream = text_stream_new2(q->getCore()->getCCore()->factory, L_STRING_TO_C(getBindIpForStream(mainTextStreamIndex)),
-			(multicastRole ==  SalMulticastReceiver) ? streamDesc->rtp_port : mediaPorts[mainTextStreamIndex].rtpPort,
-			(multicastRole ==  SalMulticastReceiver) ? 0 /* Disabled for now */ : mediaPorts[mainTextStreamIndex].rtcpPort);
-		if (multicastRole == SalMulticastReceiver)
-			joinMulticastGroup(mainTextStreamIndex, &textStream->ms);
-		
-		configureRtpSession(textStream->ms.sessions.rtp_session, LinphoneStreamTypeText);
-		setupDtlsParams(&textStream->ms);
-		media_stream_reclaim_sessions(&textStream->ms, &sessions[mainTextStreamIndex]);
-	} else
-		textStream = text_stream_new_with_sessions(q->getCore()->getCCore()->factory, &sessions[mainTextStreamIndex]);
-	if (mediaPorts[mainTextStreamIndex].rtpPort == -1)
-		setPortConfigFromRtpSession(mainTextStreamIndex, textStream->ms.sessions.rtp_session);
-
-	if (q->getCore()->getCCore()->rtptf) {
-		RtpTransport *meta_rtp;
-		RtpTransport *meta_rtcp;
-		rtp_session_get_transports(textStream->ms.sessions.rtp_session, &meta_rtp, &meta_rtcp);
-		if (!meta_rtp_transport_get_endpoint(meta_rtp))
-			meta_rtp_transport_set_endpoint(meta_rtp, q->getCore()->getCCore()->rtptf->audio_rtp_func(q->getCore()->getCCore()->rtptf->audio_rtp_func_data, mediaPorts[mainTextStreamIndex].rtpPort));
-		if (!meta_rtp_transport_get_endpoint(meta_rtcp))
-			meta_rtp_transport_set_endpoint(meta_rtcp, q->getCore()->getCCore()->rtptf->audio_rtcp_func(q->getCore()->getCCore()->rtptf->audio_rtcp_func_data, mediaPorts[mainTextStreamIndex].rtcpPort));
-	}
-
-	textStreamEvQueue = ortp_ev_queue_new();
-	rtp_session_register_event_queue(textStream->ms.sessions.rtp_session, textStreamEvQueue);
-	iceAgent->prepareIceForStream(&textStream->ms, false);
-}
-
-void MediaSessionPrivate::initializeVideoStream () {
-#ifdef VIDEO_ENABLED
-	L_Q();
-	if (videoStream)
-		return;
-	if (!sessions[mainVideoStreamIndex].rtp_session) {
-		SalMulticastRole multicastRole = getMulticastRole(SalVideo);
-		SalMediaDescription *remoteDesc = nullptr;
-		SalStreamDescription *streamDesc = nullptr;
-		if (op)
-			remoteDesc = op->getRemoteMediaDescription();
-		if (remoteDesc)
-			streamDesc = sal_media_description_find_best_stream(remoteDesc, SalVideo);
-
-		videoStream = video_stream_new2(q->getCore()->getCCore()->factory, L_STRING_TO_C(getBindIpForStream(mainVideoStreamIndex)),
-			(multicastRole ==  SalMulticastReceiver) ? streamDesc->rtp_port : mediaPorts[mainVideoStreamIndex].rtpPort,
-			(multicastRole ==  SalMulticastReceiver) ?  0 /* Disabled for now */ : mediaPorts[mainVideoStreamIndex].rtcpPort);
-		if (multicastRole == SalMulticastReceiver)
-			joinMulticastGroup(mainVideoStreamIndex, &videoStream->ms);
-		
-		configureRtpSession(videoStream->ms.sessions.rtp_session, LinphoneStreamTypeVideo);
-		setupDtlsParams(&videoStream->ms);
-		/* Initialize zrtp even if we didn't explicitely set it, just in case peer offers it */
-		if (linphone_core_media_encryption_supported(q->getCore()->getCCore(), LinphoneMediaEncryptionZRTP))
-			video_stream_enable_zrtp(videoStream, audioStream);
-
-		media_stream_reclaim_sessions(&videoStream->ms, &sessions[mainVideoStreamIndex]);
-	} else
-		videoStream = video_stream_new_with_sessions(q->getCore()->getCCore()->factory, &sessions[mainVideoStreamIndex]);
-
-	if (mediaPorts[mainVideoStreamIndex].rtpPort == -1)
-		setPortConfigFromRtpSession(mainVideoStreamIndex, videoStream->ms.sessions.rtp_session);
-	int dscp = linphone_core_get_video_dscp(q->getCore()->getCCore());
-	if (dscp!=-1)
-		video_stream_set_dscp(videoStream, dscp);
-	video_stream_enable_display_filter_auto_rotate(
-		videoStream,
-		!!lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "video", "display_filter_auto_rotate", 0)
-	);
-
-	const char *displayFilter = linphone_core_get_video_display_filter(q->getCore()->getCCore());
-	if (displayFilter)
-		video_stream_set_display_filter_name(videoStream, displayFilter);
-	video_stream_set_event_callback(videoStream, videoStreamEventCb, this);
-
-	if (q->getCore()->getCCore()->rtptf) {
-		RtpTransport *meta_rtp;
-		RtpTransport *meta_rtcp;
-		rtp_session_get_transports(videoStream->ms.sessions.rtp_session, &meta_rtp, &meta_rtcp);
-		if (!meta_rtp_transport_get_endpoint(meta_rtp)) {
-			lInfo() << "CallSession [" << q << "] using custom video RTP transport endpoint";
-			meta_rtp_transport_set_endpoint(meta_rtp, q->getCore()->getCCore()->rtptf->video_rtp_func(q->getCore()->getCCore()->rtptf->video_rtp_func_data, mediaPorts[mainVideoStreamIndex].rtpPort));
-		}
-		if (!meta_rtp_transport_get_endpoint(meta_rtcp))
-			meta_rtp_transport_set_endpoint(meta_rtcp, q->getCore()->getCCore()->rtptf->video_rtcp_func(q->getCore()->getCCore()->rtptf->video_rtcp_func_data, mediaPorts[mainVideoStreamIndex].rtcpPort));
-	}
-	videoStreamEvQueue = ortp_ev_queue_new();
-	rtp_session_register_event_queue(videoStream->ms.sessions.rtp_session, videoStreamEvQueue);
-	iceAgent->prepareIceForStream(&videoStream->ms, false);
-#ifdef TEST_EXT_RENDERER
-	video_stream_set_render_callback(videoStream, extRendererCb, nullptr);
-#endif
-#else
-	videoStream = nullptr;
-#endif
-}
-
-void MediaSessionPrivate::prepareEarlyMediaForking () {
-	/* We need to disable symmetric rtp otherwise our outgoing streams will be switching permanently between the multiple destinations */
-	if (audioStream)
-		rtp_session_set_symmetric_rtp(audioStream->ms.sessions.rtp_session, false);
-	if (videoStream)
-		rtp_session_set_symmetric_rtp(videoStream->ms.sessions.rtp_session, false);
-}
-
-void MediaSessionPrivate::postConfigureAudioStreams (bool muted) {
-	L_Q();
-	q->getCore()->getPrivate()->postConfigureAudioStream(audioStream, muted);
-	forceSpeakerMuted(speakerMuted);
-	if (linphone_core_dtmf_received_has_listener(q->getCore()->getCCore()))
-		audio_stream_play_received_dtmfs(audioStream, false);
-	if (recordActive)
-		q->startRecording();
-}
-
-void MediaSessionPrivate::setSymmetricRtp (bool value) {
-	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		setStreamSymmetricRtp(value, i);
-	}
-}
-
-void MediaSessionPrivate::setStreamSymmetricRtp(bool value, int streamIndex) {
-	MSMediaStreamSessions *mss = &sessions[streamIndex];
-	if (mss->rtp_session)
-		rtp_session_set_symmetric_rtp(mss->rtp_session, value);
-}
-
-void MediaSessionPrivate::setupRingbackPlayer () {
-	L_Q();
-	int pauseTime = 3000;
-	audio_stream_play(audioStream, q->getCore()->getCCore()->sound_conf.ringback_tone);
-	ms_filter_call_method(audioStream->soundread, MS_FILE_PLAYER_LOOP, &pauseTime);
-}
-
-void MediaSessionPrivate::startAudioStream (CallSession::State targetState) {
-	L_Q();
-	const SalStreamDescription *stream = sal_media_description_find_best_stream(resultDesc, SalAudio);
-	if (stream && (stream->dir != SalStreamInactive) && (stream->rtp_port != 0)) {
-		int usedPt = -1;
-		onHoldFile = "";
-		audioProfile = makeProfile(resultDesc, stream, &usedPt);
-		if (usedPt == -1)
-			lWarning() << "No audio stream accepted?";
-		else {
-			const char *rtpAddr = (stream->rtp_addr[0] != '\0') ? stream->rtp_addr : resultDesc->addr;
-			bool isMulticast = !!ms_is_multicast(rtpAddr);
-			bool ok = true;
-			getCurrentParams()->getPrivate()->setUsedAudioCodec(rtp_profile_get_payload(audioProfile, usedPt));
-			getCurrentParams()->enableAudio(true);
-			MSSndCard *playcard = q->getCore()->getCCore()->sound_conf.lsd_card ? q->getCore()->getCCore()->sound_conf.lsd_card : q->getCore()->getCCore()->sound_conf.play_sndcard;
-			if (!playcard)
-				lWarning() << "No card defined for playback!";
-			MSSndCard *captcard = q->getCore()->getCCore()->sound_conf.capt_sndcard;
-			if (!captcard)
-				lWarning() << "No card defined for capture!";
-			string playfile = L_C_TO_STRING(q->getCore()->getCCore()->play_file);
-			string recfile = L_C_TO_STRING(q->getCore()->getCCore()->rec_file);
-			/* Don't use file or soundcard capture when placed in recv-only mode */
-			if ((stream->rtp_port == 0) || (stream->dir == SalStreamRecvOnly) || ((stream->multicast_role == SalMulticastReceiver) && isMulticast)) {
-				captcard = nullptr;
-				playfile = "";
-			}
-			if (targetState == CallSession::State::Paused) {
-				// In paused state, we never use soundcard
-				playcard = captcard = nullptr;
-				recfile = "";
-				// And we will eventually play "playfile" if set by the user
-			}
-			if (listener && listener->isPlayingRingbackTone(q->getSharedFromThis())) {
-				captcard = nullptr;
-				playfile = ""; /* It is setup later */
-				if (lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "sound", "send_ringback_without_playback", 0) == 1) {
-					playcard = nullptr;
-					recfile = "";
-				}
-			}
-			// If playfile are supplied don't use soundcards
-			bool useRtpIo = !!lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "sound", "rtp_io", false);
-			bool useRtpIoEnableLocalOutput = !!lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "sound", "rtp_io_enable_local_output", false);
-			if (q->getCore()->getCCore()->use_files || (useRtpIo && !useRtpIoEnableLocalOutput)) {
-				captcard = playcard = nullptr;
-			}
-			if (getParams()->getPrivate()->getInConference()) {
-				// First create the graph without soundcard resources
-				captcard = playcard = nullptr;
-			}
-			if (listener && !listener->areSoundResourcesAvailable(q->getSharedFromThis())) {
-				lInfo() << "Sound resources are used by another CallSession, not using soundcard";
-				captcard = playcard = nullptr;
-			}
-
-			if (playcard) {
-				ms_snd_card_set_stream_type(playcard, MS_SND_CARD_STREAM_VOICE);
-			}
-			media_stream_set_max_network_bitrate(&audioStream->ms, linphone_core_get_upload_bandwidth(q->getCore()->getCCore()) * 1000);
-			bool useEc = captcard && linphone_core_echo_cancellation_enabled(q->getCore()->getCCore());
-			audio_stream_enable_echo_canceller(audioStream, useEc);
-			if (playcard && (stream->max_rate > 0))
-				ms_snd_card_set_preferred_sample_rate(playcard, stream->max_rate);
-			if (captcard && (stream->max_rate > 0))
-				ms_snd_card_set_preferred_sample_rate(captcard, stream->max_rate);
-			rtp_session_enable_rtcp_mux(audioStream->ms.sessions.rtp_session, stream->rtcp_mux);
-			if (!getParams()->getPrivate()->getInConference() && !getParams()->getRecordFilePath().empty()) {
-				audio_stream_mixed_record_open(audioStream, getParams()->getRecordFilePath().c_str());
-				getCurrentParams()->setRecordFilePath(getParams()->getRecordFilePath());
-			}
-			// Valid local tags are > 0
-			if (sal_stream_description_has_srtp(stream)) {
-				const SalStreamDescription *localStreamDesc = sal_media_description_find_stream(localDesc, stream->proto, SalAudio);
-				int cryptoIdx = Sal::findCryptoIndexFromTag(localStreamDesc->crypto, static_cast<unsigned char>(stream->crypto_local_tag));
-				if (cryptoIdx >= 0) {
-					ms_media_stream_sessions_set_srtp_recv_key_b64(&audioStream->ms.sessions, stream->crypto[0].algo, stream->crypto[0].master_key);
-					ms_media_stream_sessions_set_srtp_send_key_b64(&audioStream->ms.sessions, stream->crypto[0].algo, localStreamDesc->crypto[cryptoIdx].master_key);
-				} else
-					lWarning() << "Failed to find local crypto algo with tag: " << stream->crypto_local_tag;
-			}
-			configureRtpSessionForRtcpFb(stream);
-			configureRtpSessionForRtcpXr(SalAudio);
-			bool videoWillBeUsed = false;
-#if defined(VIDEO_ENABLED)
-			const SalStreamDescription *vstream = sal_media_description_find_best_stream(resultDesc, SalVideo);
-			if (vstream && (vstream->dir != SalStreamInactive) && vstream->payloads) {
-				/* When video is used, do not make adaptive rate control on audio, it is stupid */
-				videoWillBeUsed = true;
-			}
-#endif
-			configureAdaptiveRateControl(&audioStream->ms, getCurrentParams()->getUsedAudioCodec(), videoWillBeUsed);
-			if (isMulticast)
-				rtp_session_set_multicast_ttl(audioStream->ms.sessions.rtp_session, stream->ttl);
-			MSMediaStreamIO io = MS_MEDIA_STREAM_IO_INITIALIZER;
-			if (useRtpIo) {
-				if (useRtpIoEnableLocalOutput) {
-					io.input.type = MSResourceRtp;
-					io.input.session = createAudioRtpIoSession();
-					if (playcard) {
-						io.output.type = MSResourceSoundcard;
-						io.output.soundcard = playcard;
-					} else {
-						io.output.type = MSResourceFile;
-						io.output.file = recfile.empty() ? nullptr : recfile.c_str();
-					}
-				} else {
-					io.input.type = io.output.type = MSResourceRtp;
-					io.input.session = io.output.session = createAudioRtpIoSession();
-				}
-				if (!io.input.session)
-					ok = false;
-			} else {
-				if (playcard) {
-					io.output.type = MSResourceSoundcard;
-					io.output.soundcard = playcard;
-				} else {
-					io.output.type = MSResourceFile;
-					io.output.file = recfile.empty() ? nullptr : recfile.c_str();
-				}
-				if (captcard) {
-					io.input.type = MSResourceSoundcard;
-					io.input.soundcard = captcard;
-				} else {
-					io.input.type = MSResourceFile;
-					onHoldFile = playfile;
-					io.input.file = nullptr; /* We prefer to use the remote_play api, that allows to play multimedia files */
-				}
-			}
-			if (ok) {
-				currentCaptureCard = ms_media_resource_get_soundcard(&io.input);
-				currentPlayCard = ms_media_resource_get_soundcard(&io.output);
-
-				int err = audio_stream_start_from_io(audioStream, audioProfile, rtpAddr, stream->rtp_port,
-					(stream->rtcp_addr[0] != '\0') ? stream->rtcp_addr : resultDesc->addr,
-					(linphone_core_rtcp_enabled(q->getCore()->getCCore()) && !isMulticast) ? (stream->rtcp_port ? stream->rtcp_port : stream->rtp_port + 1) : 0,
-					usedPt, &io);
-				if (err == 0)
-					postConfigureAudioStreams((audioMuted || microphoneMuted) && (listener && !listener->isPlayingRingbackTone(q->getSharedFromThis())));
-			}
-			ms_media_stream_sessions_set_encryption_mandatory(&audioStream->ms.sessions, isEncryptionMandatory());
-			if ((targetState == CallSession::State::Paused) && !captcard && !playfile.empty()) {
-				int pauseTime = 500;
-				ms_filter_call_method(audioStream->soundread, MS_FILE_PLAYER_LOOP, &pauseTime);
-			}
-			if (listener && listener->isPlayingRingbackTone(q->getSharedFromThis()))
-				setupRingbackPlayer();
-			if (getParams()->getPrivate()->getInConference() && listener) {
-				// Transform the graph to connect it to the conference filter
-				bool mute = (stream->dir == SalStreamRecvOnly);
-				listener->onCallSessionConferenceStreamStarting(q->getSharedFromThis(), mute);
-			}
-			getCurrentParams()->getPrivate()->setInConference(getParams()->getPrivate()->getInConference());
-			getCurrentParams()->enableLowBandwidth(getParams()->lowBandwidthEnabled());
-			// Start ZRTP engine if needed : set here or remote have a zrtp-hash attribute
-			SalMediaDescription *remote = op->getRemoteMediaDescription();
-			const SalStreamDescription *remoteStream = sal_media_description_find_best_stream(remote, SalAudio);
-			if (linphone_core_media_encryption_supported(q->getCore()->getCCore(), LinphoneMediaEncryptionZRTP)) {
-				// Perform mutual authentication if instant messaging encryption is enabled
-				auto encryptionEngine = q->getCore()->getEncryptionEngine();
-				//Is call direction really relevant ? might be linked to offerer/answerer rather than call direction ?
-				LinphoneCallDir direction = this->getPublic()->CallSession::getDirection();
-				if (encryptionEngine && audioStream->ms.sessions.zrtp_context) {
-					encryptionEngine->mutualAuthentication(
-														   audioStream->ms.sessions.zrtp_context,
-														   op->getLocalMediaDescription(),
-														   op->getRemoteMediaDescription(),
-														   direction
-														   );
-				}
-				
-				//Start zrtp if remote has offered it or if local is configured for zrtp and is the offerrer. If not, defered when ACK is received
-				if ((getParams()->getMediaEncryption() == LinphoneMediaEncryptionZRTP && op->isOfferer()) || (remoteStream->haveZrtpHash == 1)) {
-					startZrtpPrimaryChannel(remoteStream);
-				}
-			}
-		}
-	}
-}
-
-void MediaSessionPrivate::startStreams (CallSession::State targetState) {
-	L_Q();
-	switch (targetState) {
-		case CallSession::State::IncomingEarlyMedia:
-			if (listener)
-				listener->onRingbackToneRequested(q->getSharedFromThis(), true);
-			BCTBX_NO_BREAK;
-		case CallSession::State::OutgoingEarlyMedia:
-			if (!getParams()->earlyMediaSendingEnabled()) {
-				audioMuted = true;
-				videoMuted = true;
-			}
-			break;
-		default:
-			if (listener)
-				listener->onRingbackToneRequested(q->getSharedFromThis(), false);
-			audioMuted = false;
-			videoMuted = false;
-			break;
-	}
-
-	getCurrentParams()->getPrivate()->setUsedAudioCodec(nullptr);
-	getCurrentParams()->getPrivate()->setUsedVideoCodec(nullptr);
-	getCurrentParams()->getPrivate()->setUsedRealtimeTextCodec(nullptr);
-
-	if (!audioStream && !videoStream) {
-		lFatal() << "startStreams() called without prior init!";
-		return;
-	}
-	if (iceAgent->hasSession()) {
-		/* If there is an ICE session when we are about to start streams, then ICE will conduct the media path checking and authentication properly.
-		 * Symmetric RTP must be turned off */
-		setSymmetricRtp(false);
-	}
-
-	if (audioStream) audioStartCount++;
-	if (videoStream) videoStartCount++;
-	if (textStream) textStartCount++;
-
-	lInfo() << "startStreams() CallSession=[" << q << "] local upload_bandwidth=[" << linphone_core_get_upload_bandwidth(q->getCore()->getCCore())
-		<< "] kbit/s; local download_bandwidth=[" << linphone_core_get_download_bandwidth(q->getCore()->getCCore()) << "] kbit/s";
-	getCurrentParams()->enableAudio(false);
-	if (audioStream)
-		startAudioStream(targetState);
-	else
-		lWarning() << "startStreams(): no audio stream!";
-	getCurrentParams()->enableVideo(false);
-	if (videoStream) {
-		if (audioStream)
-			audio_stream_link_video(audioStream, videoStream);
-		startVideoStream(targetState);
-	}
-	/* The on-hold file is to be played once both audio and video are ready */
-	if (!onHoldFile.empty() && !getParams()->getPrivate()->getInConference() && audioStream) {
-		MSFilter *player = audio_stream_open_remote_play(audioStream, onHoldFile.c_str());
-		if (player) {
-			int pauseTime = 500;
-			ms_filter_call_method(player, MS_PLAYER_SET_LOOP, &pauseTime);
-			ms_filter_call_method_noarg(player, MS_PLAYER_START);
-		}
-	}
-	if (getParams()->realtimeTextEnabled())
-		startTextStream();
-
-	setDtlsFingerprintOnAllStreams();
-	if (!iceAgent->hasCompleted()) {
-		iceAgent->startConnectivityChecks();
-	} else {
-		/* Should not start dtls until ice is completed */
-		startDtlsOnAllStreams();
-	}
-}
-
-void MediaSessionPrivate::startStream (SalStreamDescription *streamDesc, int streamIndex, CallSession::State targetState) {
-	L_Q();
-	string streamTypeName = sal_stream_description_get_type_as_string(streamDesc);
-
-	if (streamDesc->type == SalAudio) {
-		if (audioStream && audioStream->ms.state != MSStreamInitialized)
-			audio_stream_unprepare_sound(audioStream);
-
-		switch (targetState) {
-			case CallSession::State::IncomingEarlyMedia:
-				if (listener)
-					listener->onRingbackToneRequested(q->getSharedFromThis(), true);
-				BCTBX_NO_BREAK;
-			case CallSession::State::OutgoingEarlyMedia:
-				if (!getParams()->earlyMediaSendingEnabled())
-					audioMuted = true;
-				break;
-			default:
-				if (listener)
-					listener->onRingbackToneRequested(q->getSharedFromThis(), false);
-				audioMuted = false;
-				break;
-		}
-
-		getCurrentParams()->getPrivate()->setUsedAudioCodec(nullptr);
-
-		if (!audioStream) {
-			lFatal() << "startStream() for audio stream called without prior init!";
-			return;
-		}
-	} else if (streamDesc->type == SalVideo) {
-#ifdef VIDEO_ENABLED
-		if (videoStream && videoStream->ms.state != MSStreamInitialized)
-			video_stream_unprepare_video(videoStream);
-
-		switch (targetState) {
-			case CallSession::State::OutgoingEarlyMedia:
-				if (!getParams()->earlyMediaSendingEnabled())
-					videoMuted = true;
-				break;
-			default:
-				videoMuted = false;
-				break;
-		}
-
-		getCurrentParams()->getPrivate()->setUsedVideoCodec(nullptr);
-
-		if (!videoStream) {
-			lFatal() << "startStream() for video stream called without prior init!";
-			return;
-		}
-#endif
-	} else if (streamDesc->type == SalText) {
-		if (textStream && textStream->ms.state != MSStreamInitialized)
-			text_stream_unprepare_text(textStream);
-
-		getCurrentParams()->getPrivate()->setUsedRealtimeTextCodec(nullptr);
-	}
-
-	if (iceAgent->hasSession()) {
-		/* If there is an ICE session when we are about to start streams, then ICE will conduct the media path checking and authentication properly.
-		 * Symmetric RTP must be turned off */
-		setStreamSymmetricRtp(false, streamIndex);
-	}
-
-	lInfo() << "startStream() for " << streamTypeName << " stream CallSession=[" << q << "] local upload_bandwidth=[" << linphone_core_get_upload_bandwidth(q->getCore()->getCCore())
-		<< "] kbit/s; local download_bandwidth=[" << linphone_core_get_download_bandwidth(q->getCore()->getCCore()) << "] kbit/s";
-
-	if (streamDesc->type == SalAudio) {
-		audioStartCount++;
-
-		getCurrentParams()->enableAudio(false);
-		if (audioStream)
-			startAudioStream(targetState);
-		else
-			lWarning() << "startStreams(): no audio stream!";
-
-		postProcessHooks.push_back([this] {
-			/* The on-hold file is to be played once both audio and video are ready */
-			if (!onHoldFile.empty() && !getParams()->getPrivate()->getInConference() && audioStream) {
-				MSFilter *player = audio_stream_open_remote_play(audioStream, onHoldFile.c_str());
-				if (player) {
-					int pauseTime = 500;
-					ms_filter_call_method(player, MS_PLAYER_SET_LOOP, &pauseTime);
-					ms_filter_call_method_noarg(player, MS_PLAYER_START);
-				}
-			}
-		});
-
-		setDtlsFingerprintOnAudioStream();
-		if (iceAgent->hasCompleted())
-			startDtlsOnAudioStream();
-	} else if (streamDesc->type == SalVideo) {
-		videoStartCount++;
-
-		getCurrentParams()->enableVideo(false);
-		if (videoStream) {
-			if (audioStream)
-				audio_stream_link_video(audioStream, videoStream);
-			startVideoStream(targetState);
-		}
-
-		setDtlsFingerprintOnVideoStream();
-		if (iceAgent->hasCompleted())
-			startDtlsOnVideoStream();
-	} else if (streamDesc->type == SalText) {
-		textStartCount++;
-
-		if (getParams()->realtimeTextEnabled())
-			startTextStream();
-
-		setDtlsFingerprintOnTextStream();
-		if (iceAgent->hasCompleted())
-			startDtlsOnTextStream();
-	}
-}
-
-void MediaSessionPrivate::startTextStream () {
-	L_Q();
-	const SalStreamDescription *tstream = sal_media_description_find_best_stream(resultDesc, SalText);
-	if (tstream && (tstream->dir != SalStreamInactive) && (tstream->rtp_port != 0)) {
-		const char *rtpAddr = tstream->rtp_addr[0] != '\0' ? tstream->rtp_addr : resultDesc->addr;
-		const char *rtcpAddr = tstream->rtcp_addr[0] != '\0' ? tstream->rtcp_addr : resultDesc->addr;
-		const SalStreamDescription *localStreamDesc = sal_media_description_find_stream(localDesc, tstream->proto, SalText);
-		int usedPt = -1;
-		textProfile = makeProfile(resultDesc, tstream, &usedPt);
-		if (usedPt == -1)
-			lWarning() << "No text stream accepted";
-		else {
-			getCurrentParams()->getPrivate()->setUsedRealtimeTextCodec(rtp_profile_get_payload(textProfile, usedPt));
-			getCurrentParams()->enableRealtimeText(true);
-			unsigned int interval = getParams()->realtimeTextKeepaliveInterval();
-			getCurrentParams()->setRealtimeTextKeepaliveInterval(interval);
-			if (sal_stream_description_has_srtp(tstream)) {
-				int cryptoIdx = Sal::findCryptoIndexFromTag(localStreamDesc->crypto, static_cast<unsigned char>(tstream->crypto_local_tag));
-				if (cryptoIdx >= 0) {
-					ms_media_stream_sessions_set_srtp_recv_key_b64(&textStream->ms.sessions, tstream->crypto[0].algo, tstream->crypto[0].master_key);
-					ms_media_stream_sessions_set_srtp_send_key_b64(&textStream->ms.sessions, tstream->crypto[0].algo, localStreamDesc->crypto[cryptoIdx].master_key);
-				}
-			}
-			configureRtpSessionForRtcpFb(tstream);
-			configureRtpSessionForRtcpXr(SalText);
-			rtp_session_enable_rtcp_mux(textStream->ms.sessions.rtp_session, tstream->rtcp_mux);
-			bool isMulticast = !!ms_is_multicast(rtpAddr);
-			if (isMulticast)
-				rtp_session_set_multicast_ttl(textStream->ms.sessions.rtp_session, tstream->ttl);
-			text_stream_start(textStream, textProfile, rtpAddr, tstream->rtp_port, rtcpAddr,
-				(linphone_core_rtcp_enabled(q->getCore()->getCCore()) && !isMulticast) ? (tstream->rtcp_port ? tstream->rtcp_port : tstream->rtp_port + 1) : 0, usedPt);
-			ms_filter_call_method(textStream->rttsource, MS_RTT_4103_SOURCE_SET_KEEP_ALIVE_INTERVAL, &interval);
-			ms_filter_add_notify_callback(textStream->rttsink, realTimeTextCharacterReceived, this, false);
-			ms_media_stream_sessions_set_encryption_mandatory(&textStream->ms.sessions, isEncryptionMandatory());
-		}
-	} else
-		lInfo() << "No valid text stream defined";
-}
-
-void MediaSessionPrivate::startVideoStream (CallSession::State targetState) {
-#ifdef VIDEO_ENABLED
-	L_Q();
-	bool reusedPreview = false;
-	/* Shutdown preview */
-	MSFilter *source = nullptr;
-	if (q->getCore()->getCCore()->previewstream) {
-		if (q->getCore()->getCCore()->video_conf.reuse_preview_source)
-			source = video_preview_stop_reuse_source(q->getCore()->getCCore()->previewstream);
-		else
-			video_preview_stop(q->getCore()->getCCore()->previewstream);
-		q->getCore()->getCCore()->previewstream = nullptr;
-	}
-	const SalStreamDescription *vstream = sal_media_description_find_best_stream(resultDesc, SalVideo);
-	if (vstream && (vstream->dir != SalStreamInactive) && (vstream->rtp_port != 0)) {
-		int usedPt = -1;
-		videoProfile = makeProfile(resultDesc, vstream, &usedPt);
-		if (usedPt == -1)
-			lWarning() << "No video stream accepted";
-		else {
-			getCurrentParams()->getPrivate()->setUsedVideoCodec(rtp_profile_get_payload(videoProfile, usedPt));
-			getCurrentParams()->enableVideo(true);
-			rtp_session_enable_rtcp_mux(videoStream->ms.sessions.rtp_session, vstream->rtcp_mux);
-			media_stream_set_max_network_bitrate(&videoStream->ms, linphone_core_get_upload_bandwidth(q->getCore()->getCCore()) * 1000);
-			if (q->getCore()->getCCore()->video_conf.preview_vsize.width != 0)
-				video_stream_set_preview_size(videoStream, q->getCore()->getCCore()->video_conf.preview_vsize);
-			video_stream_set_fps(videoStream, linphone_core_get_preferred_framerate(q->getCore()->getCCore()));
-			if (lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "video", "nowebcam_uses_normal_fps", 0))
-				videoStream->staticimage_webcam_fps_optimization = false;
-			const LinphoneVideoDefinition *vdef = linphone_core_get_preferred_video_definition(q->getCore()->getCCore());
-			MSVideoSize vsize;
-			vsize.width = static_cast<int>(linphone_video_definition_get_width(vdef));
-			vsize.height = static_cast<int>(linphone_video_definition_get_height(vdef));
-			video_stream_set_sent_video_size(videoStream, vsize);
-			video_stream_enable_self_view(videoStream, q->getCore()->getCCore()->video_conf.selfview);
-			if (videoWindowId)
-				video_stream_set_native_window_id(videoStream, videoWindowId);
-			else if (q->getCore()->getCCore()->video_window_id)
-				video_stream_set_native_window_id(videoStream, q->getCore()->getCCore()->video_window_id);
-			if (q->getCore()->getCCore()->preview_window_id)
-				video_stream_set_native_preview_window_id(videoStream, q->getCore()->getCCore()->preview_window_id);
-			video_stream_use_preview_video_window(videoStream, q->getCore()->getCCore()->use_preview_window);
-			const char *rtpAddr = (vstream->rtp_addr[0] != '\0') ? vstream->rtp_addr : resultDesc->addr;
-			const char *rtcpAddr = (vstream->rtcp_addr[0] != '\0') ? vstream->rtcp_addr : resultDesc->addr;
-			bool isMulticast = !!ms_is_multicast(rtpAddr);
-			MediaStreamDir dir = MediaStreamSendRecv;
-			bool isActive = true;
-			if (isMulticast) {
-				if (vstream->multicast_role == SalMulticastReceiver)
-					dir = MediaStreamRecvOnly;
-				else
-					dir = MediaStreamSendOnly;
-			} else if ((vstream->dir == SalStreamSendOnly) && q->getCore()->getCCore()->video_conf.capture)
-				dir = MediaStreamSendOnly;
-			else if ((vstream->dir == SalStreamRecvOnly) && q->getCore()->getCCore()->video_conf.display)
-				dir = MediaStreamRecvOnly;
-			else if (vstream->dir == SalStreamSendRecv) {
-				if (q->getCore()->getCCore()->video_conf.display && q->getCore()->getCCore()->video_conf.capture)
-					dir = MediaStreamSendRecv;
-				else if (q->getCore()->getCCore()->video_conf.display)
-					dir = MediaStreamRecvOnly;
-				else
-					dir = MediaStreamSendOnly;
-			} else {
-				lWarning() << "Video stream is inactive";
-				/* Either inactive or incompatible with local capabilities */
-				isActive = false;
-			}
-			MSWebCam *cam = getVideoDevice();
-			if (isActive) {
-				if (sal_stream_description_has_srtp(vstream)) {
-					const SalStreamDescription *localStreamDesc = sal_media_description_find_stream(localDesc, vstream->proto, SalVideo);
-					int cryptoIdx = Sal::findCryptoIndexFromTag(localStreamDesc->crypto, static_cast<unsigned char>(vstream->crypto_local_tag));
-					if (cryptoIdx >= 0) {
-						ms_media_stream_sessions_set_srtp_recv_key_b64(&videoStream->ms.sessions, vstream->crypto[0].algo, vstream->crypto[0].master_key);
-						ms_media_stream_sessions_set_srtp_send_key_b64(&videoStream->ms.sessions, vstream->crypto[0].algo, localStreamDesc->crypto[cryptoIdx].master_key);
-					}
-				}
-				configureRtpSessionForRtcpFb(vstream);
-				configureRtpSessionForRtcpXr(SalVideo);
-				configureAdaptiveRateControl(&videoStream->ms, getCurrentParams()->getUsedVideoCodec(), true);
-				log->video_enabled = true;
-				video_stream_set_direction(videoStream, dir);
-				lInfo() << "startVideoStream: device_rotation=" << q->getCore()->getCCore()->device_rotation;
-				video_stream_set_device_rotation(videoStream, q->getCore()->getCCore()->device_rotation);
-				video_stream_set_freeze_on_error(videoStream, !!lp_config_get_int(linphone_core_get_config(q->getCore()->getCCore()), "video", "freeze_on_error", 1));
-				if (isMulticast)
-					rtp_session_set_multicast_ttl(videoStream->ms.sessions.rtp_session, vstream->ttl);
-				video_stream_use_video_preset(videoStream, lp_config_get_string(linphone_core_get_config(q->getCore()->getCCore()), "video", "preset", nullptr));
-				if (q->getCore()->getCCore()->video_conf.reuse_preview_source && source) {
-					lInfo() << "video_stream_start_with_source kept: " << source;
-					video_stream_start_with_source(videoStream, videoProfile, rtpAddr, vstream->rtp_port, rtcpAddr,
-						linphone_core_rtcp_enabled(q->getCore()->getCCore()) ? (vstream->rtcp_port ? vstream->rtcp_port : vstream->rtp_port + 1) : 0,
-						usedPt, -1, cam, source);
-					reusedPreview = true;
-				} else {
-					bool ok = true;
-					MSMediaStreamIO io = MS_MEDIA_STREAM_IO_INITIALIZER;
-					if (linphone_config_get_bool(linphone_core_get_config(q->getCore()->getCCore()), "video", "rtp_io", FALSE)) {
-						io.input.type = io.output.type = MSResourceRtp;
-						io.input.session = io.output.session = createVideoRtpIoSession();
-						if (!io.input.session) {
-							ok = false;
-							lWarning() << "Cannot create video RTP IO session";
-						}
-					} else {
-						io.input.type = MSResourceCamera;
-						io.input.camera = cam;
-						io.output.type = MSResourceDefault;
-					}
-					if (ok) {
-						video_stream_start_from_io(videoStream, videoProfile, rtpAddr, vstream->rtp_port, rtcpAddr,
-							(linphone_core_rtcp_enabled(q->getCore()->getCCore()) && !isMulticast)  ? (vstream->rtcp_port ? vstream->rtcp_port : vstream->rtp_port + 1) : 0,
-							usedPt, &io);
-					}
-				}
-
-				ms_media_stream_sessions_set_encryption_mandatory(&videoStream->ms.sessions, isEncryptionMandatory());
-				if (listener)
-					listener->onResetFirstVideoFrameDecoded(q->getSharedFromThis());
-				/* Start ZRTP engine if needed : set here or remote have a zrtp-hash attribute */
-				SalMediaDescription *remote = op->getRemoteMediaDescription();
-				const SalStreamDescription *remoteStream = sal_media_description_find_best_stream(remote, SalVideo);
-				if ((getParams()->getMediaEncryption() == LinphoneMediaEncryptionZRTP) || (remoteStream->haveZrtpHash == 1)) {
-					/* Audio stream is already encrypted and video stream is active */
-					if (media_stream_secured(&audioStream->ms) && (media_stream_get_state(&videoStream->ms) == MSStreamStarted)) {
-						video_stream_start_zrtp(videoStream);
-						if (remoteStream->haveZrtpHash == 1) {
-							int retval = ms_zrtp_setPeerHelloHash(videoStream->ms.sessions.zrtp_context, (uint8_t *)remoteStream->zrtphash, strlen((const char *)(remoteStream->zrtphash)));
-							if (retval != 0)
-								lError() << "Video stream ZRTP hash mismatch 0x" << hex << retval;
-						}
-					}
-				}
-
-				if (linphone_core_retransmission_on_nack_enabled(q->getCore()->getCCore())) {
-					video_stream_enable_retransmission_on_nack(videoStream, TRUE);
-				}
-			}
-		}
-	} else
-		lInfo() << "No valid video stream defined";
-	if (!reusedPreview && source) {
-		/* Destroy not-reused source filter */
-		lWarning() << "Video preview (" << source << ") not reused: destroying it";
-		ms_filter_destroy(source);
-	}
-#endif
-}
-
-void MediaSessionPrivate::stopAudioStream () {
-	L_Q();
-	if (!audioStream)
-		return;
-
-	if (listener)
-		listener->onUpdateMediaInfoForReporting(q->getSharedFromThis(), LINPHONE_CALL_STATS_AUDIO);
-	media_stream_reclaim_sessions(&audioStream->ms, &sessions[mainAudioStreamIndex]);
-	if (audioStream->ec) {
-		char *stateStr = nullptr;
-		ms_filter_call_method(audioStream->ec, MS_ECHO_CANCELLER_GET_STATE_STRING, &stateStr);
-		if (stateStr) {
-			lInfo() << "Writing echo canceler state, " << (int)strlen(stateStr) << " bytes";
-			lp_config_write_relative_file(linphone_core_get_config(q->getCore()->getCCore()), ecStateStore.c_str(), stateStr);
-		}
-	}
-	audio_stream_get_local_rtp_stats(audioStream, &log->local_stats);
-	fillLogStats(&audioStream->ms);
-	if (listener)
-		listener->onCallSessionConferenceStreamStopping(q->getSharedFromThis());
-	ms_bandwidth_controller_remove_stream(q->getCore()->getCCore()->bw_controller, &audioStream->ms);
-	audio_stream_stop(audioStream);
-	updateRtpStats(audioStats, mainAudioStreamIndex);
-	audioStream = nullptr;
-	handleStreamEvents(mainAudioStreamIndex);
-	rtp_session_unregister_event_queue(sessions[mainAudioStreamIndex].rtp_session, audioStreamEvQueue);
-	ortp_ev_queue_flush(audioStreamEvQueue);
-	ortp_ev_queue_destroy(audioStreamEvQueue);
-	audioStreamEvQueue = nullptr;
-
-	getCurrentParams()->getPrivate()->setUsedAudioCodec(nullptr);
-
-	currentCaptureCard = nullptr;
-	currentPlayCard = nullptr;
-
-}
-
-void MediaSessionPrivate::stopTextStream () {
-	L_Q();
-	if (textStream) {
-		if (listener)
-			listener->onUpdateMediaInfoForReporting(q->getSharedFromThis(), LINPHONE_CALL_STATS_TEXT);
-		media_stream_reclaim_sessions(&textStream->ms, &sessions[mainTextStreamIndex]);
-		fillLogStats(&textStream->ms);
-		text_stream_stop(textStream);
-		updateRtpStats(textStats, mainTextStreamIndex);
-		textStream = nullptr;
-		handleStreamEvents(mainTextStreamIndex);
-		rtp_session_unregister_event_queue(sessions[mainTextStreamIndex].rtp_session, textStreamEvQueue);
-		ortp_ev_queue_flush(textStreamEvQueue);
-		ortp_ev_queue_destroy(textStreamEvQueue);
-		textStreamEvQueue = nullptr;
-		getCurrentParams()->getPrivate()->setUsedRealtimeTextCodec(nullptr);
-	}
-}
-
-void MediaSessionPrivate::stopVideoStream () {
-#ifdef VIDEO_ENABLED
-	L_Q();
-	if (videoStream) {
-		if (listener)
-			listener->onUpdateMediaInfoForReporting(q->getSharedFromThis(), LINPHONE_CALL_STATS_VIDEO);
-		media_stream_reclaim_sessions(&videoStream->ms, &sessions[mainVideoStreamIndex]);
-		fillLogStats(&videoStream->ms);
-		ms_bandwidth_controller_remove_stream(q->getCore()->getCCore()->bw_controller, &videoStream->ms);
-		video_stream_stop(videoStream);
-		updateRtpStats(videoStats, mainVideoStreamIndex);
-		videoStream = nullptr;
-		handleStreamEvents(mainVideoStreamIndex);
-		rtp_session_unregister_event_queue(sessions[mainVideoStreamIndex].rtp_session, videoStreamEvQueue);
-		ortp_ev_queue_flush(videoStreamEvQueue);
-		ortp_ev_queue_destroy(videoStreamEvQueue);
-		videoStreamEvQueue = nullptr;
-		getCurrentParams()->getPrivate()->setUsedVideoCodec(nullptr);
-	}
-#endif
-}
-
-void MediaSessionPrivate::tryEarlyMediaForking (SalMediaDescription *md) {
-	L_Q();
-	lInfo() << "Early media response received from another branch, checking if media can be forked to this new destination";
-	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&resultDesc->streams[i]))
-			continue;
-		SalStreamDescription *refStream = &resultDesc->streams[i];
-		SalStreamDescription *newStream = &md->streams[i];
-		if ((refStream->type == newStream->type) && refStream->payloads && newStream->payloads) {
-			OrtpPayloadType *refpt = reinterpret_cast<OrtpPayloadType *>(refStream->payloads->data);
-			OrtpPayloadType *newpt = reinterpret_cast<OrtpPayloadType *>(newStream->payloads->data);
-			if ((strcmp(refpt->mime_type, newpt->mime_type) == 0) && (refpt->clock_rate == newpt->clock_rate)
-				&& (payload_type_get_number(refpt) == payload_type_get_number(newpt))) {
-				MediaStream *ms = nullptr;
-				if (refStream->type == SalAudio)
-					ms = &audioStream->ms;
-				else if (refStream->type == SalVideo)
-					ms = &videoStream->ms;
-				if (ms) {
-					RtpSession *session = ms->sessions.rtp_session;
-					const char *rtpAddr = (newStream->rtp_addr[0] != '\0') ? newStream->rtp_addr : md->addr;
-					const char *rtcpAddr = (newStream->rtcp_addr[0] != '\0') ? newStream->rtcp_addr : md->addr;
-					if (ms_is_multicast(rtpAddr))
-						lInfo() << "Multicast addr [" << rtpAddr << "/" << newStream->rtp_port << "] does not need auxiliary rtp's destination for CallSession [" << q << "]";
-					else
-						rtp_session_add_aux_remote_addr_full(session, rtpAddr, newStream->rtp_port, rtcpAddr, newStream->rtcp_port);
-				}
-			}
-		}
-	}
-}
-
-void MediaSessionPrivate::updateStreamFrozenPayloads (SalStreamDescription *resultDesc, SalStreamDescription *localStreamDesc) {
-	L_Q();
-	for (bctbx_list_t *elem = resultDesc->payloads; elem != nullptr; elem = bctbx_list_next(elem)) {
-		OrtpPayloadType *pt = reinterpret_cast<OrtpPayloadType *>(bctbx_list_get_data(elem));
-		if (PayloadTypeHandler::isPayloadTypeNumberAvailable(localStreamDesc->already_assigned_payloads, payload_type_get_number(pt), nullptr)) {
-			/* New codec, needs to be added to the list */
-			localStreamDesc->already_assigned_payloads = bctbx_list_append(localStreamDesc->already_assigned_payloads, payload_type_clone(pt));
-			lInfo() << "CallSession[" << q << "] : payload type " << payload_type_get_number(pt) << " " << pt->mime_type << "/" << pt->clock_rate
-				<< " fmtp=" << L_C_TO_STRING(pt->recv_fmtp) << " added to frozen list";
-		}
-	}
-}
-
-void MediaSessionPrivate::updateFrozenPayloads (SalMediaDescription *result) {
-	for (int i = 0; i < result->nb_streams; i++) {
-		updateStreamFrozenPayloads(&result->streams[i], &localDesc->streams[i]);
-	}
-}
-
-void MediaSessionPrivate::updateStreams (SalMediaDescription *newMd, CallSession::State targetState) {
-	L_Q();
-
-	if (state == CallSession::State::Connected || state == CallSession::State::Resuming ||
-		(state == CallSession::State::IncomingEarlyMedia && !linphone_core_get_ring_during_incoming_early_media(q->getCore()->getCCore()))) {
-		q->getCore()->getPrivate()->getToneManager()->goToCall(q->getSharedFromThis());
-	}
-
-	if (!newMd) {
-		lError() << "updateStreams() called with null media description";
-		return;
-	}
-
-	updateBiggestDesc(localDesc);
-	sal_media_description_ref(newMd);
-	SalMediaDescription *oldMd = resultDesc;
-	resultDesc = newMd;
-
-	if (getParams()->getMediaEncryption() == LinphoneMediaEncryptionDTLS) {
-		getCurrentParams()->getPrivate()->setUpdateCallWhenIceCompleted(false);
-		lInfo() << "Disabling update call when ice completed on call [" << q << "]";
-	}
-
-	if ((audioStream && (audioStream->ms.state == MSStreamStarted)) || (videoStream && (videoStream->ms.state == MSStreamStarted))) {
-		clearEarlyMediaDestinations();
-
-		/* We already started media: check if we really need to restart it */
-		int mdChanged = 0;
-		if (oldMd) {
-			mdChanged = mediaParametersChanged(oldMd, newMd);
-			/* Might not be mandatory to restart stream for each ice restart as it leads bad user experience, specially in video. See 0002495 for better background on this */
-			if (mdChanged & (SAL_MEDIA_DESCRIPTION_CODEC_CHANGED
-				| SAL_MEDIA_DESCRIPTION_NETWORK_XXXCAST_CHANGED
-				| SAL_MEDIA_DESCRIPTION_ICE_RESTART_DETECTED
-				| SAL_MEDIA_DESCRIPTION_FORCE_STREAM_RECONSTRUCTION)
-			) {
-				lInfo() << "Media descriptions are different, need to restart the streams";
-			} else {
-				for(int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; ++i) {
-					if (!sal_stream_description_active(&oldMd->streams[i]) && !sal_stream_description_active(&newMd->streams[i])) continue;
-					string streamTypeName = sal_stream_description_get_type_as_string(&newMd->streams[i]);
-
-					/* If there was a change in the streams then newMd should have more streams */
-					if (mdChanged & SAL_MEDIA_DESCRIPTION_STREAMS_CHANGED && i >= oldMd->nb_streams) {
-						lInfo() << "New " << streamTypeName << " stream detected, starting the stream";
-
-						if (newMd->streams[i].type == SalAudio) {
-							initializeAudioStream();
-						} else if (newMd->streams[i].type == SalVideo) {
-							initializeVideoStream();
-						} else if (newMd->streams[i].type == SalText) {
-							initializeTextStream();
-						}
-
-						if (getParams()->earlyMediaSendingEnabled() && (state == CallSession::State::OutgoingEarlyMedia)) {
-							if (newMd->streams[i].type == SalAudio && audioStream)
-								rtp_session_set_symmetric_rtp(audioStream->ms.sessions.rtp_session, false);
-							else if (newMd->streams[i].type == SalVideo && videoStream)
-								rtp_session_set_symmetric_rtp(videoStream->ms.sessions.rtp_session, false);
-						}
-
-						startStream(&newMd->streams[i], i, targetState);
-
-						if (newMd->streams[i].type == SalAudio && audioStream) {
-							if ((state == CallSession::State::Pausing) && pausedByApp && (q->getCore()->getCallCount() == 1))
-								q->getCore()->getPrivate()->getToneManager()->startNamedTone(q->getSharedFromThis(), LinphoneToneCallOnHold);
-						}
-
-						updateStreamFrozenPayloads(&newMd->streams[i], &localDesc->streams[i]);
-
-						continue;
-					}
-
-					int sdChanged = sal_stream_description_equals(&oldMd->streams[i], &newMd->streams[i]);
-
-					if (newMd->streams[i].type == SalAudio && listener && listener->isPlayingRingbackTone(q->getSharedFromThis())) {
-						lInfo() << "Playing ringback tone, will restart the audio stream";
-						sdChanged |= SAL_MEDIA_DESCRIPTION_FORCE_STREAM_RECONSTRUCTION;
-					}
-
-					if (sdChanged & (SAL_MEDIA_DESCRIPTION_CODEC_CHANGED
-						| SAL_MEDIA_DESCRIPTION_NETWORK_XXXCAST_CHANGED
-						| SAL_MEDIA_DESCRIPTION_ICE_RESTART_DETECTED
-						| SAL_MEDIA_DESCRIPTION_FORCE_STREAM_RECONSTRUCTION)
-					) {
-						lInfo() << "Stream descriptions are different, need to restart the " << streamTypeName << " stream";
-						restartStream(&newMd->streams[i], i, sdChanged, targetState);
-					} else {
-						sdChanged |= mdChanged;
-
-						if (newMd->streams[i].type == SalAudio && audioMuted && (targetState == CallSession::State::StreamsRunning)) {
-							lInfo() << "Early media finished, unmuting audio input...";
-							/* We were in early media, now we want to enable real media */
-							audioMuted = false;
-
-							if (audioStream) {
-								linphone_core_enable_mic(q->getCore()->getCCore(), linphone_core_mic_enabled(q->getCore()->getCCore()));
-							}
-						}
-
-#ifdef VIDEO_ENABLED
-						if (newMd->streams[i].type == SalVideo && videoMuted && (targetState == CallSession::State::StreamsRunning)) {
-							lInfo() << "Early media finished, unmuting video input...";
-							/* We were in early media, now we want to enable real media */
-							videoMuted = false;
-
-							if (videoStream && cameraEnabled) {
-								q->enableCamera(q->cameraEnabled());
-							}
-						}
-#endif
-
-						if (sdChanged == SAL_MEDIA_DESCRIPTION_UNCHANGED) {
-							/* FIXME ZRTP, might be restarted in any cases? */
-							lInfo() << "No need to restart the " << streamTypeName << " stream, SDP is unchanged";
-						} else {
-							if (sdChanged & SAL_MEDIA_DESCRIPTION_NETWORK_CHANGED) {
-								lInfo() << "Network parameters have changed for the " << streamTypeName << " stream, update it";
-								if (newMd->streams[i].type == SalAudio || newMd->streams[i].type == SalVideo) {
-									updateStreamDestination(newMd, &newMd->streams[i]);
-								}
-							}
-							if (sdChanged & SAL_MEDIA_DESCRIPTION_CRYPTO_KEYS_CHANGED) {
-								lInfo() << "Crypto parameters have changed for the " << streamTypeName << " stream, update it";
-								updateStreamCryptoParameters(&oldMd->streams[i], &newMd->streams[i]);
-							}
-						}
-					}
-				}
-
-				for (const auto &hook : postProcessHooks) {
-					hook();
-				}
-				postProcessHooks.clear();
-
-				if (!iceAgent->hasCompleted()) {
-					iceAgent->startConnectivityChecks();
-				}
-
-				if (oldMd)
-					sal_media_description_unref(oldMd);
-
-				return;
-			}
-		}
-
-		stopStreams();
-		if (mdChanged & SAL_MEDIA_DESCRIPTION_NETWORK_XXXCAST_CHANGED) {
-			lInfo() << "Media ip type has changed, destroying sessions context on CallSession [" << q << "]";
-			ms_media_stream_sessions_uninit(&sessions[mainAudioStreamIndex]);
-			ms_media_stream_sessions_uninit(&sessions[mainVideoStreamIndex]);
-			ms_media_stream_sessions_uninit(&sessions[mainTextStreamIndex]);
-		}
-		initializeStreams();
-	}
-
-	if (!audioStream) {
-		/* This happens after pausing the call locally. The streams are destroyed and then we wait the 200Ok to recreate them */
-		initializeStreams();
-	}
-
-	if (getParams()->earlyMediaSendingEnabled() && (state == CallSession::State::OutgoingEarlyMedia))
-		prepareEarlyMediaForking();
-
-	startStreams(targetState);
-
-	if ((state == CallSession::State::Pausing) && pausedByApp && (q->getCore()->getCallCount() == 1)) {
-		q->getCore()->getPrivate()->getToneManager()->startNamedTone(q->getSharedFromThis(), LinphoneToneCallOnHold);
-	}
-
-	updateFrozenPayloads(newMd);
-
-	upBandwidth = linphone_core_get_upload_bandwidth(q->getCore()->getCCore());
-
-	if (oldMd)
-		sal_media_description_unref(oldMd);
-}
-
-void MediaSessionPrivate::updateStreamDestination (SalMediaDescription *newMd, SalStreamDescription *newDesc) {
-	if (!sal_stream_description_active(newDesc))
-		return;
-
-	if (newDesc && newDesc->type == SalAudio) {
-		if (audioStream) {
-			const char *rtpAddr = (newDesc->rtp_addr[0] != '\0') ? newDesc->rtp_addr : newMd->addr;
-			const char *rtcpAddr = (newDesc->rtcp_addr[0] != '\0') ? newDesc->rtcp_addr : newMd->addr;
-			lInfo() << "Change audio stream destination: RTP=" << rtpAddr << ":" << newDesc->rtp_port << " RTCP=" << rtcpAddr << ":" << newDesc->rtcp_port;
-			rtp_session_set_remote_addr_full(audioStream->ms.sessions.rtp_session, rtpAddr, newDesc->rtp_port, rtcpAddr, newDesc->rtcp_port);
-		}
-	}
-#ifdef VIDEO_ENABLED
-	else if (newDesc && newDesc->type == SalVideo) {
-		if (videoStream) {
-			const char *rtpAddr = (newDesc->rtp_addr[0] != '\0') ? newDesc->rtp_addr : newMd->addr;
-			const char *rtcpAddr = (newDesc->rtcp_addr[0] != '\0') ? newDesc->rtcp_addr : newMd->addr;
-			lInfo() << "Change video stream destination: RTP=" << rtpAddr << ":" << newDesc->rtp_port << " RTCP=" << rtcpAddr << ":" << newDesc->rtcp_port;
-			rtp_session_set_remote_addr_full(videoStream->ms.sessions.rtp_session, rtpAddr, newDesc->rtp_port, rtcpAddr, newDesc->rtcp_port);
-		}
-	}
-#endif
-}
-
-void MediaSessionPrivate::updateStreamsDestinations (SalMediaDescription *oldMd, SalMediaDescription *newMd) {
-	SalStreamDescription *newAudioDesc = nullptr;
-
-	#ifdef VIDEO_ENABLED
-		SalStreamDescription *newVideoDesc = nullptr;
-	#endif
-
-	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&newMd->streams[i]))
-			continue;
-		if (newMd->streams[i].type == SalAudio)
-			newAudioDesc = &newMd->streams[i];
-
-		#ifdef VIDEO_ENABLED
-			else if (newMd->streams[i].type == SalVideo)
-				newVideoDesc = &newMd->streams[i];
-		#endif
-	}
-
-	updateStreamDestination(newMd, newAudioDesc);
-#ifdef VIDEO_ENABLED
-	updateStreamDestination(newMd, newVideoDesc);
-#endif
-}
-
-// -----------------------------------------------------------------------------
-
-bool MediaSessionPrivate::allStreamsAvpfEnabled () const {
-	int nbActiveStreams = 0;
-	int nbAvpfEnabledStreams = 0;
-	if (audioStream && media_stream_get_state(&audioStream->ms) == MSStreamStarted) {
-		nbActiveStreams++;
-		if (media_stream_avpf_enabled(&audioStream->ms))
-			nbAvpfEnabledStreams++;
-	}
-	if (videoStream && media_stream_get_state(&videoStream->ms) == MSStreamStarted) {
-		nbActiveStreams++;
-		if (media_stream_avpf_enabled(&videoStream->ms))
-			nbAvpfEnabledStreams++;
-	}
-	return (nbActiveStreams > 0) && (nbActiveStreams == nbAvpfEnabledStreams);
-}
-
-bool MediaSessionPrivate::allStreamsEncrypted () const {
-	int numberOfEncryptedStreams = 0;
-	int numberOfActiveStreams = 0;
-	if (audioStream && (media_stream_get_state(&audioStream->ms) == MSStreamStarted)) {
-		numberOfActiveStreams++;
-		if (media_stream_secured(&audioStream->ms))
-			numberOfEncryptedStreams++;
-	}
-	if (videoStream && (media_stream_get_state(&videoStream->ms) == MSStreamStarted)) {
-		numberOfActiveStreams++;
-		if (media_stream_secured(&videoStream->ms))
-			numberOfEncryptedStreams++;
-	}
-	if (textStream && (media_stream_get_state(&textStream->ms) == MSStreamStarted)) {
-		numberOfActiveStreams++;
-		if (media_stream_secured(&textStream->ms))
-			numberOfEncryptedStreams++;
-	}
-	return (numberOfActiveStreams > 0) && (numberOfActiveStreams == numberOfEncryptedStreams);
-}
-
-bool MediaSessionPrivate::atLeastOneStreamStarted () const {
-	return (audioStream && (media_stream_get_state(&audioStream->ms) == MSStreamStarted))
-		|| (videoStream && (media_stream_get_state(&videoStream->ms) == MSStreamStarted))
-		|| (textStream && (media_stream_get_state(&textStream->ms) == MSStreamStarted));
-}
-
-void MediaSessionPrivate::audioStreamAuthTokenReady (const string &authToken, bool verified) {
-	this->authToken = authToken;
-	authTokenVerified = verified;
-	lInfo() << "Authentication token is " << authToken << "(" << (verified ? "verified" : "unverified") << ")";
-}
-
-void MediaSessionPrivate::audioStreamEncryptionChanged (bool encrypted) {
-	propagateEncryptionChanged();
-
-	#ifdef VIDEO_ENABLED
-		L_Q();
-		/* Enable video encryption */
-		if ((getParams()->getMediaEncryption() == LinphoneMediaEncryptionZRTP) && q->getCurrentParams()->videoEnabled()) {
-			lInfo() << "Trying to start ZRTP encryption on video stream";
-			video_stream_start_zrtp(videoStream);
-		}
-	#endif
-}
-
-uint16_t MediaSessionPrivate::getAvpfRrInterval () const {
-	uint16_t rrInterval = 0;
-	if (audioStream && (media_stream_get_state(&audioStream->ms) == MSStreamStarted)) {
-		uint16_t streamRrInterval = media_stream_get_avpf_rr_interval(&audioStream->ms);
-		if (streamRrInterval > rrInterval) rrInterval = streamRrInterval;
-	}
-	if (videoStream && (media_stream_get_state(&videoStream->ms) == MSStreamStarted)) {
-		uint16_t streamRrInterval = media_stream_get_avpf_rr_interval(&videoStream->ms);
-		if (streamRrInterval > rrInterval) rrInterval = streamRrInterval;
-	}
-	return rrInterval;
-}
-
-unsigned int MediaSessionPrivate::getNbActiveStreams () const {
-	SalMediaDescription *md = nullptr;
-	if (op)
-		md = op->getRemoteMediaDescription();
-	if (!md)
-		return 0;
-	return sal_media_description_nb_active_streams_of_type(md, SalAudio) + sal_media_description_nb_active_streams_of_type(md, SalVideo) + sal_media_description_nb_active_streams_of_type(md, SalText);
-}
-
-bool MediaSessionPrivate::isEncryptionMandatory () const {
-	L_Q();
-	if (getParams()->getMediaEncryption() == LinphoneMediaEncryptionDTLS) {
-		lInfo() << "Forced encryption mandatory on CallSession [" << q << "] due to SRTP-DTLS";
-		return true;
-	}
-	return getParams()->mandatoryMediaEncryptionEnabled();
-}
-
-int MediaSessionPrivate::mediaParametersChanged (SalMediaDescription *oldMd, SalMediaDescription *newMd) {
-	L_Q();
-	if (forceStreamsReconstruction) {
-		forceStreamsReconstruction = false;
-		return SAL_MEDIA_DESCRIPTION_FORCE_STREAM_RECONSTRUCTION;
-	}
-	if (getParams()->getPrivate()->getInConference() != getCurrentParams()->getPrivate()->getInConference())
-		return SAL_MEDIA_DESCRIPTION_FORCE_STREAM_RECONSTRUCTION;
-	if (upBandwidth != linphone_core_get_upload_bandwidth(q->getCore()->getCCore()))
-		return SAL_MEDIA_DESCRIPTION_FORCE_STREAM_RECONSTRUCTION;
-	if (localDescChanged) {
-		char *differences = sal_media_description_print_differences(localDescChanged);
-		lInfo() << "Local description has changed: " << differences;
-		ms_free(differences);
-	}
-	int otherDescChanged = sal_media_description_global_equals(oldMd, newMd);
-	if (otherDescChanged) {
-		char *differences = sal_media_description_print_differences(otherDescChanged);
-		lInfo() << "Other description has changed: " << differences;
-		ms_free(differences);
-	}
-	return localDescChanged | otherDescChanged;
+	return getParams()->mandatoryMediaEncryptionEnabled();
 }
 
 void MediaSessionPrivate::propagateEncryptionChanged () {
 	L_Q();
-	if (!allStreamsEncrypted()) {
+	
+	string authToken = getStreamsGroup().getAuthenticationToken();
+	bool authTokenVerified = getStreamsGroup().getAuthenticationTokenVerified();
+	if (!getStreamsGroup().allStreamsEncrypted()) {
 		lInfo() << "Some streams are not encrypted";
 		getCurrentParams()->setMediaEncryption(LinphoneMediaEncryptionNone);
 		if (listener)
@@ -4014,7 +1584,15 @@ void MediaSessionPrivate::propagateEncryptionChanged () {
 			if (encryptionEngine && authTokenVerified) {
 				const SalAddress *remoteAddress = getOp()->getRemoteContactAddress();
 				peerDeviceId = sal_address_as_string_uri_only(remoteAddress);
-				encryptionEngine->authenticationVerified(audioStream->ms.sessions.zrtp_context, op->getRemoteMediaDescription(), peerDeviceId);
+				Stream *stream = mainAudioStreamIndex != -1 ? getStreamsGroup().getStream(mainAudioStreamIndex) : nullptr;
+				if (stream){
+					MS2Stream *ms2s = dynamic_cast<MS2Stream*>(stream);
+					if (ms2s){
+						encryptionEngine->authenticationVerified(ms2s->getZrtpContext(), op->getRemoteMediaDescription(), peerDeviceId);
+					}else{
+						lError() << "Could not dynamic_cast to MS2Stream in propagateEncryptionChanged().";
+					}
+				}
 				ms_free(peerDeviceId);
 			}
 		} else {
@@ -4027,115 +1605,34 @@ void MediaSessionPrivate::propagateEncryptionChanged () {
 				: (q->getCurrentParams()->getMediaEncryption() == LinphoneMediaEncryptionDTLS) ? "DTLS" : "Unknown mechanism");
 		if (listener)
 			listener->onEncryptionChanged(q->getSharedFromThis(), true, authToken);
-#ifdef VIDEO_ENABLED
-		if (isEncryptionMandatory() && videoStream && media_stream_started(&videoStream->ms)) {
+
+		Stream *videoStream = mainVideoStreamIndex != -1 ? getStreamsGroup().getStream(mainVideoStreamIndex) : nullptr;
+		if (isEncryptionMandatory() && videoStream && videoStream->getState() == Stream::Running) {
 			/* Nothing could have been sent yet so generating key frame */
-			video_stream_send_vfu(videoStream);
+			VideoControlInterface *vc = dynamic_cast<VideoControlInterface*> (videoStream);
+			if (vc) vc->sendVfu();
 		}
-#endif
-	}
-}
-
-// -----------------------------------------------------------------------------
-
-void MediaSessionPrivate::fillLogStats (MediaStream *st) {
-	float quality = media_stream_get_average_quality_rating(st);
-	if (quality >= 0) {
-		if (static_cast<int>(log->quality) == -1)
-			log->quality = quality;
-		else
-			log->quality *= quality / 5.0f;
 	}
 }
 
-void MediaSessionPrivate::updateRtpStats (LinphoneCallStats *stats, int streamIndex) {
-	if (sessions[streamIndex].rtp_session) {
-		const rtp_stats_t *rtpStats = rtp_session_get_stats(sessions[streamIndex].rtp_session);
-		if (rtpStats)
-			_linphone_call_stats_set_rtp_stats(stats, rtpStats);
-	}
+MSWebCam *MediaSessionPrivate::getVideoDevice()const{
+#ifdef VIDEO_ENABLED
+	MS2VideoStream *vs = getStreamsGroup().lookupMainStreamInterface<MS2VideoStream>(SalVideo);
+	if (vs) return vs->getVideoDevice(state);
+#endif
+	return nullptr;
 }
 
 // -----------------------------------------------------------------------------
 
-void MediaSessionPrivate::executeBackgroundTasks (bool oneSecondElapsed) {
-	L_Q();
-	switch (state) {
-	case CallSession::State::StreamsRunning:
-	case CallSession::State::OutgoingEarlyMedia:
-	case CallSession::State::IncomingEarlyMedia:
-	case CallSession::State::PausedByRemote:
-	case CallSession::State::Paused:
-		if (oneSecondElapsed) {
-			float audioLoad = 0.f;
-			float videoLoad = 0.f;
-			float textLoad = 0.f;
-			if (audioStream && audioStream->ms.sessions.ticker)
-				audioLoad = ms_ticker_get_average_load(audioStream->ms.sessions.ticker);
-			if (videoStream && videoStream->ms.sessions.ticker)
-				videoLoad = ms_ticker_get_average_load(videoStream->ms.sessions.ticker);
-			if (textStream && textStream->ms.sessions.ticker)
-				textLoad = ms_ticker_get_average_load(textStream->ms.sessions.ticker);
-			reportBandwidth();
-			lInfo() << "Thread processing load: audio=" << audioLoad << "\tvideo=" << videoLoad << "\ttext=" << textLoad;
-		}
-		break;
-	default:
-		/* No stats for other states */
-		break;
-	}
-
-	handleStreamEvents(mainAudioStreamIndex);
-	handleStreamEvents(mainVideoStreamIndex);
-	handleStreamEvents(mainTextStreamIndex);
-
-	if (listener)
-		listener->onNoMediaTimeoutCheck(q->getSharedFromThis(), oneSecondElapsed);
-}
-
-void MediaSessionPrivate::reportBandwidth () {
-	L_Q();
-	reportBandwidthForStream(&audioStream->ms, LinphoneStreamTypeAudio);
-	reportBandwidthForStream(&videoStream->ms, LinphoneStreamTypeVideo);
-	reportBandwidthForStream(&textStream->ms, LinphoneStreamTypeText);
-
-	lInfo() << "Bandwidth usage for CallSession [" << q << "]:\n" << fixed << setprecision(2) <<
-		"\tRTP  audio=[d=" << linphone_call_stats_get_download_bandwidth(audioStats) << ",u=" << linphone_call_stats_get_upload_bandwidth(audioStats) <<
-		"], video=[d=" << linphone_call_stats_get_download_bandwidth(videoStats) << ",u=" << linphone_call_stats_get_upload_bandwidth(videoStats) << ",ed=" << linphone_call_stats_get_estimated_download_bandwidth(videoStats) <<
-		"], text=[d=" << linphone_call_stats_get_download_bandwidth(textStats) << ",u=" << linphone_call_stats_get_upload_bandwidth(textStats) << "] kbits/sec\n" <<
-		"\tRTCP audio=[d=" << linphone_call_stats_get_rtcp_download_bandwidth(audioStats) << ",u=" << linphone_call_stats_get_rtcp_upload_bandwidth(audioStats) <<
-		"], video=[d=" << linphone_call_stats_get_rtcp_download_bandwidth(videoStats) << ",u=" << linphone_call_stats_get_rtcp_upload_bandwidth(videoStats) <<
-		"], text=[d=" << linphone_call_stats_get_rtcp_download_bandwidth(textStats) << ",u=" << linphone_call_stats_get_rtcp_upload_bandwidth(textStats) << "] kbits/sec";
-}
 
-void MediaSessionPrivate::reportBandwidthForStream (MediaStream *ms, LinphoneStreamType type) {
-	L_Q();
-	LinphoneCallStats *stats = nullptr;
-	if (type == LinphoneStreamTypeAudio) {
-		stats = audioStats;
-	} else if (type == LinphoneStreamTypeVideo) {
-		stats = videoStats;
-	} else if (type == LinphoneStreamTypeText) {
-		stats = textStats;
-	} else
-		return;
 
-	bool active = ms ? (media_stream_get_state(ms) == MSStreamStarted) : false;
-	_linphone_call_stats_set_download_bandwidth(stats, active ? (float)(media_stream_get_down_bw(ms) * 1e-3) : 0.f);
-	_linphone_call_stats_set_upload_bandwidth(stats, active ? (float)(media_stream_get_up_bw(ms) * 1e-3) : 0.f);
-	_linphone_call_stats_set_rtcp_download_bandwidth(stats, active ? (float)(media_stream_get_rtcp_down_bw(ms) * 1e-3) : 0.f);
-	_linphone_call_stats_set_rtcp_upload_bandwidth(stats, active ? (float)(media_stream_get_rtcp_up_bw(ms) * 1e-3) : 0.f);
-	_linphone_call_stats_set_ip_family_of_remote(stats,
-		active ? (ortp_stream_is_ipv6(&ms->sessions.rtp_session->rtp.gs) ? LinphoneAddressFamilyInet6 : LinphoneAddressFamilyInet) : LinphoneAddressFamilyUnspec);
-
-	if (q->getCore()->getCCore()->send_call_stats_periodical_updates) {
-		if (active)
-			linphone_call_stats_update(stats, ms);
-		_linphone_call_stats_set_updated(stats, _linphone_call_stats_get_updated(stats) | LINPHONE_CALL_STATS_PERIODICAL_UPDATE);
-		if (listener)
-			listener->onStatsUpdated(q->getSharedFromThis(), stats);
-		_linphone_call_stats_set_updated(stats, 0);
-	}
+// -----------------------------------------------------------------------------
+
+void MediaSessionPrivate::lossOfMediaDetected() {
+	L_Q();
+	if (listener)
+		listener->onLossOfMediaDetected(q->getSharedFromThis());
 }
 
 // -----------------------------------------------------------------------------
@@ -4161,16 +1658,6 @@ void MediaSessionPrivate::handleIncomingReceivedStateInIncomingNotification () {
 	acceptOrTerminateReplacedSessionInIncomingNotification();
 }
 
-bool MediaSessionPrivate::isReadyForInvite () const {
-	bool callSessionReady = CallSessionPrivate::isReadyForInvite();
-	bool iceReady = false;
-	if (iceAgent->hasSession()) {
-		if (iceAgent->candidatesGathered())
-			iceReady = true;
-	} else
-		iceReady = true;
-	return callSessionReady && iceReady;
-}
 
 LinphoneStatus MediaSessionPrivate::pause () {
 	L_Q();
@@ -4189,20 +1676,19 @@ LinphoneStatus MediaSessionPrivate::pause () {
 	}
 	broken = false;
 	setState(CallSession::State::Pausing, "Pausing call");
-	makeLocalMediaDescription();
-	op->setLocalMediaDescription(localDesc);
+	makeLocalMediaDescription(true);
 	op->update(subject.c_str(), false);
 	if (listener)
 		listener->onResetCurrentSession(q->getSharedFromThis());
-	if (audioStream || videoStream || textStream)
-		stopStreams();
+	stopStreams();
 	pausedByApp = false;
 	return 0;
 }
 
 int MediaSessionPrivate::restartInvite () {
 	stopStreams();
-	initializeStreams();
+	getStreamsGroup().clearStreams();
+	makeLocalMediaDescription(true);
 	return CallSessionPrivate::restartInvite();
 }
 
@@ -4212,16 +1698,8 @@ void MediaSessionPrivate::setTerminated () {
 }
 
 LinphoneStatus MediaSessionPrivate::startAcceptUpdate (CallSession::State nextState, const string &stateInfo) {
-	if (iceAgent->hasSession() && (iceAgent->getNbLosingPairs() > 0)) {
-		/* Defer the sending of the answer until there are no losing pairs left */
-		return 0;
-	}
-	makeLocalMediaDescription();
-	updateRemoteSessionIdAndVer();
-	op->setLocalMediaDescription(localDesc);
 	op->accept();
 	SalMediaDescription *md = op->getFinalMediaDescription();
-	iceAgent->stopIceForInactiveStreams(md);
 	if (md && !sal_media_description_empty(md))
 		updateStreams(md, nextState);
 	setState(nextState, stateInfo);
@@ -4230,12 +1708,8 @@ LinphoneStatus MediaSessionPrivate::startAcceptUpdate (CallSession::State nextSt
 
 LinphoneStatus MediaSessionPrivate::startUpdate (const string &subject) {
 	L_Q();
-	fillMulticastMediaAddresses();
-	if (!getParams()->getPrivate()->getNoUserConsent())
-		makeLocalMediaDescription();
-	if (!q->getCore()->getCCore()->sip_conf.sdp_200_ack)
-		op->setLocalMediaDescription(localDesc);
-	else
+	
+	if (q->getCore()->getCCore()->sip_conf.sdp_200_ack)
 		op->setLocalMediaDescription(nullptr);
 	LinphoneStatus result = CallSessionPrivate::startUpdate(subject);
 	if (q->getCore()->getCCore()->sip_conf.sdp_200_ack) {
@@ -4256,24 +1730,30 @@ void MediaSessionPrivate::terminate () {
 void MediaSessionPrivate::updateCurrentParams () const {
 	CallSessionPrivate::updateCurrentParams();
 
-	LinphoneVideoDefinition *vdef = linphone_video_definition_new(MS_VIDEO_SIZE_UNKNOWN_W, MS_VIDEO_SIZE_UNKNOWN_H, nullptr);
-	getCurrentParams()->getPrivate()->setSentVideoDefinition(vdef);
-	getCurrentParams()->getPrivate()->setReceivedVideoDefinition(vdef);
-	linphone_video_definition_unref(vdef);
-#ifdef VIDEO_ENABLED
-	if (videoStream) {
-		MSVideoSize vsize = video_stream_get_sent_video_size(videoStream);
-		vdef = linphone_video_definition_new(static_cast<unsigned int>(vsize.width), static_cast<unsigned int>(vsize.height), nullptr);
+	
+	VideoControlInterface *i = getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+	if (i){
+		VideoControlInterface::VideoStats st;
+		LinphoneVideoDefinition *vdef;
+		
+		i->getRecvStats(&st);
+		vdef = linphone_video_definition_new((unsigned)st.width, (unsigned)st.height, nullptr);
+		getCurrentParams()->getPrivate()->setReceivedVideoDefinition(vdef);
+		linphone_video_definition_unref(vdef);
+		getCurrentParams()->getPrivate()->setReceivedFps(st.fps);
+		
+		i->getSendStats(&st);
+		vdef = linphone_video_definition_new((unsigned)st.width, (unsigned)st.height, nullptr);
 		getCurrentParams()->getPrivate()->setSentVideoDefinition(vdef);
 		linphone_video_definition_unref(vdef);
-		vsize = video_stream_get_received_video_size(videoStream);
-		vdef = linphone_video_definition_new(static_cast<unsigned int>(vsize.width), static_cast<unsigned int>(vsize.height), nullptr);
+		getCurrentParams()->getPrivate()->setSentFps(st.fps);
+		
+	}else{
+		LinphoneVideoDefinition *vdef = linphone_video_definition_new(MS_VIDEO_SIZE_UNKNOWN_W, MS_VIDEO_SIZE_UNKNOWN_H, nullptr);
+		getCurrentParams()->getPrivate()->setSentVideoDefinition(vdef);
 		getCurrentParams()->getPrivate()->setReceivedVideoDefinition(vdef);
 		linphone_video_definition_unref(vdef);
-		getCurrentParams()->getPrivate()->setSentFps(video_stream_get_sent_framerate(videoStream));
-		getCurrentParams()->getPrivate()->setReceivedFps(video_stream_get_received_framerate(videoStream));
 	}
-#endif
 
 	/* REVISITED
 	 * Previous code was buggy.
@@ -4283,7 +1763,7 @@ void MediaSessionPrivate::updateCurrentParams () const {
 	 * mechanism  (added by jehan: and encryption status from media which is much stronger than only result of offer/answer )
 	 * Typically there can be inactive streams for which the media layer has no idea of whether they are encrypted or not.
 	 */
-
+	string authToken = getStreamsGroup().getAuthenticationToken();
 	switch (getParams()->getMediaEncryption()) {
 		case LinphoneMediaEncryptionZRTP:
 			if (atLeastOneStreamStarted()) {
@@ -4325,37 +1805,41 @@ void MediaSessionPrivate::updateCurrentParams () const {
 	else
 		getCurrentParams()->setAvpfRrInterval(0);
 	if (md) {
-		SalStreamDescription *sd = sal_media_description_find_best_stream(md, SalAudio);
-		getCurrentParams()->setAudioDirection(sd ? MediaSessionParamsPrivate::salStreamDirToMediaDirection(sd->dir) : LinphoneMediaDirectionInactive);
-		if (getCurrentParams()->getAudioDirection() != LinphoneMediaDirectionInactive) {
-			const char *rtpAddr = (sd->rtp_addr[0] != '\0') ? sd->rtp_addr : md->addr;
-			getCurrentParams()->enableAudioMulticast(!!ms_is_multicast(rtpAddr));
-		} else
-			getCurrentParams()->enableAudioMulticast(false);
-		sd = sal_media_description_find_best_stream(md, SalVideo);
-		getCurrentParams()->getPrivate()->enableImplicitRtcpFb(sd && sal_stream_description_has_implicit_avpf(sd));
-		getCurrentParams()->setVideoDirection(sd ? MediaSessionParamsPrivate::salStreamDirToMediaDirection(sd->dir) : LinphoneMediaDirectionInactive);
-		if (getCurrentParams()->getVideoDirection() != LinphoneMediaDirectionInactive) {
-			const char *rtpAddr = (sd->rtp_addr[0] != '\0') ? sd->rtp_addr : md->addr;
-			getCurrentParams()->enableVideoMulticast(!!ms_is_multicast(rtpAddr));
-		} else
-			getCurrentParams()->enableVideoMulticast(false);
+		if (mainAudioStreamIndex != -1){
+			SalStreamDescription *sd = &md->streams[mainAudioStreamIndex];
+			getCurrentParams()->setAudioDirection(sd ? MediaSessionParamsPrivate::salStreamDirToMediaDirection(sd->dir) : LinphoneMediaDirectionInactive);
+			if (getCurrentParams()->getAudioDirection() != LinphoneMediaDirectionInactive) {
+				const char *rtpAddr = (sd->rtp_addr[0] != '\0') ? sd->rtp_addr : md->addr;
+				getCurrentParams()->enableAudioMulticast(!!ms_is_multicast(rtpAddr));
+			} else
+				getCurrentParams()->enableAudioMulticast(false);
+			getCurrentParams()->enableAudio(sal_stream_description_enabled(sd));
+		}
+		if (mainVideoStreamIndex != -1){
+			SalStreamDescription *sd = &md->streams[mainVideoStreamIndex];
+			getCurrentParams()->getPrivate()->enableImplicitRtcpFb(sd && sal_stream_description_has_implicit_avpf(sd));
+			getCurrentParams()->setVideoDirection(sd ? MediaSessionParamsPrivate::salStreamDirToMediaDirection(sd->dir) : LinphoneMediaDirectionInactive);
+			if (getCurrentParams()->getVideoDirection() != LinphoneMediaDirectionInactive) {
+				const char *rtpAddr = (sd->rtp_addr[0] != '\0') ? sd->rtp_addr : md->addr;
+				getCurrentParams()->enableVideoMulticast(!!ms_is_multicast(rtpAddr));
+			} else
+				getCurrentParams()->enableVideoMulticast(false);
+			getCurrentParams()->enableVideo(sal_stream_description_enabled(sd));
+		}
+		if (mainTextStreamIndex != -1){
+			SalStreamDescription *sd = &md->streams[mainTextStreamIndex];
+			// Direction and multicast are not supported for real-time text.
+			getCurrentParams()->enableRealtimeText(sal_stream_description_enabled(sd));
+		}
 	}
+	getCurrentParams()->getPrivate()->setUpdateCallWhenIceCompleted(getParams()->getPrivate()->getUpdateCallWhenIceCompleted());
 }
 
 // -----------------------------------------------------------------------------
 
-void MediaSessionPrivate::accept (const MediaSessionParams *msp, bool wasRinging) {
-	L_Q();
-	if (msp) {
-		setParams(new MediaSessionParams(*msp));
-		iceAgent->prepare(localDesc, true, false /*we don't allow gathering now, it must have been done before*/);
-		makeLocalMediaDescription();
-		op->setLocalMediaDescription(localDesc);
-	}
-
-	updateRemoteSessionIdAndVer();
 
+void MediaSessionPrivate::startAccept(){
+	L_Q();
 	/* Give a chance a set card prefered sampling frequency */
 	if (localDesc->streams[0].max_rate > 0) {
 		lInfo() << "Configuring prefered card sampling rate to [" << localDesc->streams[0].max_rate << "]";
@@ -4365,20 +1849,30 @@ void MediaSessionPrivate::accept (const MediaSessionParams *msp, bool wasRinging
 			ms_snd_card_set_preferred_sample_rate(q->getCore()->getCCore()->sound_conf.capt_sndcard, localDesc->streams[0].max_rate);
 	}
 
-	LinphoneCore *lc = q->getCore()->getCCore();
-	if (!wasRinging && (audioStream->ms.state == MSStreamInitialized) && !lc->use_files) {
-		audio_stream_prepare_sound(audioStream, lc->sound_conf.play_sndcard, lc->sound_conf.capt_sndcard);
-	}
-
 	CallSessionPrivate::accept(nullptr);
 
 	SalMediaDescription *newMd = op->getFinalMediaDescription();
-	iceAgent->stopIceForInactiveStreams(newMd);
 	if (newMd) {
 		updateStreams(newMd, CallSession::State::StreamsRunning);
 		setState(CallSession::State::StreamsRunning, "Connected (streams running)");
 	} else
 		expectMediaInAck = true;
+	if (callAcceptanceDefered) callAcceptanceDefered = false;
+}
+
+void MediaSessionPrivate::accept (const MediaSessionParams *msp, bool wasRinging) {
+	if (msp) {
+		setParams(new MediaSessionParams(*msp));
+	}
+	if (msp || localDesc == nullptr) makeLocalMediaDescription(op->getRemoteMediaDescription() ? false : true);
+
+	updateRemoteSessionIdAndVer();
+
+	if (getStreamsGroup().prepare()){
+		callAcceptanceDefered = true;
+		return; /* Deferred until completion of ICE gathering */
+	}
+	startAccept();
 }
 
 LinphoneStatus MediaSessionPrivate::acceptUpdate (const CallSessionParams *csp, CallSession::State nextState, const string &stateInfo) {
@@ -4409,11 +1903,10 @@ LinphoneStatus MediaSessionPrivate::acceptUpdate (const CallSessionParams *csp,
 		lWarning() << "Video isn't supported in conference";
 		getParams()->enableVideo(false);
 	}
-	/* Update multicast params according to call params */
-	fillMulticastMediaAddresses();
-	iceAgent->checkSession(IR_Controlled, true);
-	initializeStreams(); /* So that video stream is initialized if necessary */
-	if (iceAgent->prepare(localDesc, true))
+	updateRemoteSessionIdAndVer();
+	makeLocalMediaDescription(op->getRemoteMediaDescription() ? false : true);
+
+	if (getStreamsGroup().prepare())
 		return 0; /* Deferred until completion of ICE gathering */
 	startAcceptUpdate(nextState, stateInfo);
 	return 0;
@@ -4422,87 +1915,32 @@ LinphoneStatus MediaSessionPrivate::acceptUpdate (const CallSessionParams *csp,
 // -----------------------------------------------------------------------------
 
 void MediaSessionPrivate::refreshSockets () {
-	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		MSMediaStreamSessions *mss = &sessions[i];
-		if (mss->rtp_session)
-			rtp_session_refresh_sockets(mss->rtp_session);
-	}
+	getStreamsGroup().refreshSockets();
 }
 
 void MediaSessionPrivate::reinviteToRecoverFromConnectionLoss () {
 	L_Q();
 	lInfo() << "MediaSession [" << q << "] is going to be updated (reINVITE) in order to recover from lost connectivity";
-	if (iceAgent->hasSession())
-		iceAgent->resetSession(IR_Controlling);
+	getStreamsGroup().getIceService().resetSession();
 	q->update(getParams());
 }
 
 void MediaSessionPrivate::repairByInviteWithReplaces () {
 	if ((state == CallSession::State::IncomingEarlyMedia) || (state == CallSession::State::OutgoingEarlyMedia)) {
 		stopStreams();
-		initializeStreams();
 	}
 	CallSessionPrivate::repairByInviteWithReplaces();
 }
 
-// -----------------------------------------------------------------------------
-
-#ifdef VIDEO_ENABLED
-void MediaSessionPrivate::videoStreamEventCb (const MSFilter *f, const unsigned int eventId, const void *args) {
-	L_Q();
-	switch (eventId) {
-		case MS_VIDEO_DECODER_DECODING_ERRORS:
-			lWarning() << "MS_VIDEO_DECODER_DECODING_ERRORS";
-			if (videoStream && video_stream_is_decoding_error_to_be_reported(videoStream, 5000)) {
-				video_stream_decoding_error_reported(videoStream);
-				q->sendVfuRequest();
-			}
-			break;
-		case MS_VIDEO_DECODER_RECOVERED_FROM_ERRORS:
-			lInfo() << "MS_VIDEO_DECODER_RECOVERED_FROM_ERRORS";
-			if (videoStream)
-				video_stream_decoding_error_recovered(videoStream);
-			break;
-		case MS_VIDEO_DECODER_FIRST_IMAGE_DECODED:
-			lInfo() << "First video frame decoded successfully";
-			if (listener)
-				listener->onFirstVideoFrameDecoded(q->getSharedFromThis());
-			break;
-		case MS_VIDEO_DECODER_SEND_PLI:
-		case MS_VIDEO_DECODER_SEND_SLI:
-		case MS_VIDEO_DECODER_SEND_RPSI:
-			/* Handled internally by mediastreamer2 */
-			break;
-		case MS_CAMERA_PREVIEW_SIZE_CHANGED: {
-			MSVideoSize size = *(MSVideoSize *)args;
-			lInfo() << "Camera video preview size changed: " << size.width << "x" << size.height;
-			linphone_core_resize_video_preview(q->getCore()->getCCore(), size.width, size.height);
-			break;
-		}
-		default:
-			lWarning() << "Unhandled event " << eventId;
-			break;
-	}
-}
-#endif
-
-void MediaSessionPrivate::realTimeTextCharacterReceived (MSFilter *f, unsigned int id, void *arg) {
-	L_Q();
-	if (id == MS_RTT_4103_RECEIVED_CHAR) {
-		RealtimeTextReceivedCharacter *data = reinterpret_cast<RealtimeTextReceivedCharacter *>(arg);
-		if (listener)
-			listener->onRealTimeTextCharacterReceived(q->getSharedFromThis(), data);
-	}
-}
-
 int MediaSessionPrivate::sendDtmf () {
 	L_Q();
 	LinphoneCore *lc = q->getCore()->getCCore();
 	// By default we send DTMF RFC2833 if we do not have enabled SIP_INFO but we can also send RFC2833 and SIP_INFO
 	if (linphone_core_get_use_rfc2833_for_dtmf(lc) || !linphone_core_get_use_info_for_dtmf(lc)) {
+		AudioControlInterface *i = getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
 		// In Band DTMF
-		if (audioStream)
-			audio_stream_send_dtmf(audioStream, dtmfSequence.front());
+		if (i)
+			i->sendDtmf(dtmfSequence.front());
 		else {
 			lError() << "Cannot send RFC2833 DTMF when we are not in communication";
 			return FALSE;
@@ -4526,12 +1964,12 @@ int MediaSessionPrivate::sendDtmf () {
 // -----------------------------------------------------------------------------
 
 int MediaSessionPrivate::resumeAfterFailedTransfer (void *userData, unsigned int) {
-	MediaSession *session = reinterpret_cast<MediaSession *>(userData);
+	MediaSession *session = static_cast<MediaSession *>(userData);
 	return session->getPrivate()->resumeAfterFailedTransfer();
 }
 
 bool_t MediaSessionPrivate::startPendingRefer (void *userData) {
-	MediaSession *session = reinterpret_cast<MediaSession *>(userData);
+	MediaSession *session = static_cast<MediaSession *>(userData);
 	session->getPrivate()->startPendingRefer();
 	return TRUE;
 }
@@ -4575,6 +2013,10 @@ void MediaSessionPrivate::stunAuthRequestedCb (const char *realm, const char *no
 	*username = user;
 }
 
+IceSession *MediaSessionPrivate::getIceSession()const{
+	return getIceService().getSession();
+}
+
 // =============================================================================
 
 MediaSession::MediaSession (const shared_ptr<Core> &core, shared_ptr<Participant> me, const CallSessionParams *params, CallSessionListener *listener)
@@ -4588,24 +2030,8 @@ MediaSession::MediaSession (const shared_ptr<Core> &core, shared_ptr<Participant
 	else
 		d->setParams(new MediaSessionParams());
 	d->setCurrentParams(new MediaSessionParams());
-
-	d->audioStats = _linphone_call_stats_new();
-	d->initStats(d->audioStats, LinphoneStreamTypeAudio);
-	d->videoStats = _linphone_call_stats_new();
-	d->initStats(d->videoStats, LinphoneStreamTypeVideo);
-	d->textStats = _linphone_call_stats_new();
-	d->initStats(d->textStats, LinphoneStreamTypeText);
-
-	int minPort, maxPort;
-	linphone_core_get_audio_port_range(getCore()->getCCore(), &minPort, &maxPort);
-	d->setPortConfig(d->mainAudioStreamIndex, make_pair(minPort, maxPort));
-	linphone_core_get_video_port_range(getCore()->getCCore(), &minPort, &maxPort);
-	d->setPortConfig(d->mainVideoStreamIndex, make_pair(minPort, maxPort));
-	linphone_core_get_text_port_range(getCore()->getCCore(), &minPort, &maxPort);
-	d->setPortConfig(d->mainTextStreamIndex, make_pair(minPort, maxPort));
-
-	memset(d->sessions, 0, sizeof(d->sessions));
-	d->iceAgent = makeUnique<IceAgent>(*this);
+	d->streamsGroup = makeUnique<StreamsGroup>(*this);
+	d->streamsGroup->getIceService().setListener(d);
 
 	lInfo() << "New MediaSession [" << this << "] initialized (LinphoneCore version: " << linphone_core_get_version() << ")";
 }
@@ -4613,14 +2039,7 @@ MediaSession::MediaSession (const shared_ptr<Core> &core, shared_ptr<Participant
 MediaSession::~MediaSession () {
 	L_D();
 	cancelDtmfs();
-	if (d->audioStream || d->videoStream)
-		d->freeResources();
-	if (d->audioStats)
-		linphone_call_stats_unref(d->audioStats);
-	if (d->videoStats)
-		linphone_call_stats_unref(d->videoStats);
-	if (d->textStats)
-		linphone_call_stats_unref(d->textStats);
+	d->freeResources();
 	if (d->natPolicy)
 		linphone_nat_policy_unref(d->natPolicy);
 	if (d->localDesc)
@@ -4662,8 +2081,7 @@ LinphoneStatus MediaSession::acceptEarlyMedia (const MediaSessionParams *msp) {
 	/* If parameters are passed, update the media description */
 	if (msp) {
 		d->setParams(new MediaSessionParams(*msp));
-		d->makeLocalMediaDescription();
-		d->op->setLocalMediaDescription(d->localDesc);
+		d->makeLocalMediaDescription(false);
 		d->op->setSentCustomHeaders(d->getParams()->getPrivate()->getCustomHeaders());
 	}
 	d->op->notifyRinging(true);
@@ -4706,12 +2124,10 @@ void MediaSession::configure (LinphoneCallDir direction, LinphoneProxyConfig *cf
 
 	if (direction == LinphoneCallOutgoing) {
 		d->selectOutgoingIpVersion();
-		d->getLocalIp(to);
-		d->initializeStreams(); // Reserve the sockets immediately
-		d->getCurrentParams()->getPrivate()->setUpdateCallWhenIceCompleted(d->getParams()->getPrivate()->getUpdateCallWhenIceCompleted());
-		d->fillMulticastMediaAddresses();
-		if (d->natPolicy && linphone_nat_policy_ice_enabled(d->natPolicy))
-			d->iceAgent->checkSession(IR_Controlling, false);
+		if (!getCore()->getCCore()->sip_conf.sdp_200_ack){
+			/* Do not make a local media description when sending an empty INVITE. */
+			d->makeLocalMediaDescription(true);
+		}
 		d->runStunTestsIfNeeded();
 		d->discoverMtu(to);
 	} else if (direction == LinphoneCallIncoming) {
@@ -4720,22 +2136,10 @@ void MediaSession::configure (LinphoneCallDir direction, LinphoneProxyConfig *cf
 		 * remote offer, if any. If the remote offer contains IPv4 addresses, we should propose IPv4 as well. */
 		Address cleanedFrom = from;
 		cleanedFrom.clean();
-		d->getLocalIp(cleanedFrom);
 		d->setParams(new MediaSessionParams());
-		d->params->initDefault(getCore());
+		d->params->initDefault(getCore(), LinphoneCallIncoming);
 		d->initializeParamsAccordingToIncomingCallParams();
-		SalMediaDescription *md = d->op->getRemoteMediaDescription();
-		if (d->natPolicy && linphone_nat_policy_ice_enabled(d->natPolicy)) {
-			if (md) {
-				/* Create the ice session now if ICE is required */
-				d->iceAgent->checkSession(IR_Controlled, false);
-			} else {
-				linphone_nat_policy_unref(d->natPolicy);
-				d->natPolicy = nullptr;
-				lWarning() << "ICE not supported for incoming INVITE without SDP";
-			}
-		}
-		d->initializeStreams(); // Reserve the sockets immediately
+		d->makeLocalMediaDescription(op->getRemoteMediaDescription() ? false : true);
 		if (d->natPolicy)
 			d->runStunTestsIfNeeded();
 		d->discoverMtu(cleanedFrom);
@@ -4759,31 +2163,42 @@ LinphoneStatus MediaSession::deferUpdate () {
 void MediaSession::initiateIncoming () {
 	L_D();
 	CallSession::initiateIncoming();
-	d->initializeStreams();
 	if (d->natPolicy) {
-		if (linphone_nat_policy_ice_enabled(d->natPolicy))
-			d->deferIncomingNotification = d->iceAgent->prepare(d->localDesc, true);
+		if (linphone_nat_policy_ice_enabled(d->natPolicy)){
+			
+			d->deferIncomingNotification = d->getStreamsGroup().prepare();
+			/* 
+			 * If ICE gathering is done, we can update the local media description immediately.
+			 * Otherwise, we'll get the ORTP_EVENT_ICE_GATHERING_FINISHED event later.
+			 */
+			if (!d->deferIncomingNotification) d->updateLocalMediaDescriptionFromIce();
+		}
 	}
 }
 
 bool MediaSession::initiateOutgoing () {
 	L_D();
 	bool defer = CallSession::initiateOutgoing();
-	d->initializeStreams();
 	if (linphone_nat_policy_ice_enabled(d->natPolicy)) {
 		if (getCore()->getCCore()->sip_conf.sdp_200_ack)
 			lWarning() << "ICE is not supported when sending INVITE without SDP";
 		else {
 			/* Defer the start of the call after the ICE gathering process */
-			defer |= d->iceAgent->prepare(d->localDesc, false);
+			bool ice_needs_defer = d->getStreamsGroup().prepare();
+			if (!ice_needs_defer) {
+				/* 
+				 * If ICE gathering is done, we can update the local media description immediately.
+				 * Otherwise, we'll get the ORTP_EVENT_ICE_GATHERING_FINISHED event later.
+				 */
+				d->updateLocalMediaDescriptionFromIce();
+			}
+			defer |= ice_needs_defer;
 		}
 	}
 	return defer;
 }
 
 void MediaSession::iterate (time_t currentRealTime, bool oneSecondElapsed) {
-	L_D();
-	d->executeBackgroundTasks(oneSecondElapsed);
 	CallSession::iterate(currentRealTime, oneSecondElapsed);
 }
 
@@ -4813,20 +2228,19 @@ LinphoneStatus MediaSession::resume () {
 	d->broken = false;
 	/* Stop playing music immediately. If remote side is a conference it
 	 * prevents the participants to hear it while the 200OK comes back. */
-	if (d->audioStream)
-		audio_stream_play(d->audioStream, nullptr);
-	d->makeLocalMediaDescription();
+	Stream *as = d->getStreamsGroup().lookupMainStream(SalAudio);
+	if (as) as->stop();
+	d->setState(CallSession::State::Resuming,"Resuming");
+	d->makeLocalMediaDescription(true);
 	sal_media_description_set_dir(d->localDesc, SalStreamSendRecv);
-	if (!getCore()->getCCore()->sip_conf.sdp_200_ack)
-		d->op->setLocalMediaDescription(d->localDesc);
-	else
+	if (getCore()->getCCore()->sip_conf.sdp_200_ack)
 		d->op->setLocalMediaDescription(nullptr);
 	string subject = "Call resuming";
 	if (d->getParams()->getPrivate()->getInConference() && !getCurrentParams()->getPrivate()->getInConference())
 		subject = "Conference";
 	if (d->op->update(subject.c_str(), false) != 0)
 		return -1;
-	d->setState(CallSession::State::Resuming,"Resuming");
+	
 	if (!d->getParams()->getPrivate()->getInConference() && d->listener)
 		d->listener->onSetCurrentSession(getSharedFromThis());
 	if (getCore()->getCCore()->sip_conf.sdp_200_ack) {
@@ -4861,27 +2275,23 @@ LinphoneStatus MediaSession::sendDtmfs (const std::string &dtmfs) {
 }
 
 void MediaSession::sendVfuRequest () {
-#ifdef VIDEO_ENABLED
 	L_D();
 	MediaSessionParams *curParams = getCurrentParams();
 
-	if ((curParams->avpfEnabled() || curParams->getPrivate()->implicitRtcpFbEnabled())
-		&& d->videoStream && media_stream_get_state(&d->videoStream->ms) == MSStreamStarted) { // || sal_media_description_has_implicit_avpf((const SalMediaDescription *)call->resultdesc)
+	if ((curParams->avpfEnabled() || curParams->getPrivate()->implicitRtcpFbEnabled())) { // || sal_media_description_has_implicit_avpf((const SalMediaDescription *)call->resultdesc)
 		lInfo() << "Request Full Intra Request on CallSession [" << this << "]";
-		video_stream_send_fir(d->videoStream);
+		d->getStreamsGroup().forEach<VideoControlInterface>([](VideoControlInterface *i){ i->sendVfuRequest(); });
 	} else if (getCore()->getCCore()->sip_conf.vfu_with_info) {
 		lInfo() << "Request SIP INFO FIR on CallSession [" << this << "]";
 		if (d->state == CallSession::State::StreamsRunning)
 			d->op->sendVfuRequest();
 	} else
 		lInfo() << "vfu request using sip disabled from config [sip,vfu_with_info]";
-#endif
 }
 
 void MediaSession::startIncomingNotification (bool notifyRinging) {
 	L_D();
-	d->makeLocalMediaDescription();
-	d->op->setLocalMediaDescription(d->localDesc);
+	
 	SalMediaDescription *md = d->op->getFinalMediaDescription();
 	if (md) {
 		if (sal_media_description_empty(md) || linphone_core_incompatible_security(getCore()->getCCore(), md)) {
@@ -4900,30 +2310,21 @@ void MediaSession::startIncomingNotification (bool notifyRinging) {
 int MediaSession::startInvite (const Address *destination, const string &subject, const Content *content) {
 	L_D();
 	linphone_core_stop_dtmf_stream(getCore()->getCCore());
-	d->makeLocalMediaDescription();
 	if (!getCore()->getCCore()->ringstream && getCore()->getCCore()->sound_conf.play_sndcard && getCore()->getCCore()->sound_conf.capt_sndcard) {
 		/* Give a chance to set card prefered sampling frequency */
-		if (d->localDesc->streams[0].max_rate > 0)
+		if (d->localDesc && d->localDesc->streams[0].max_rate > 0)
 			ms_snd_card_set_preferred_sample_rate(getCore()->getCCore()->sound_conf.play_sndcard, d->localDesc->streams[0].max_rate);
-		if (!getCore()->getCCore()->use_files)
-			audio_stream_prepare_sound(d->audioStream, getCore()->getCCore()->sound_conf.play_sndcard, getCore()->getCCore()->sound_conf.capt_sndcard);
-	}
-	if (!getCore()->getCCore()->sip_conf.sdp_200_ack) {
-		/* We are offering, set local media description before sending the call */
-		d->op->setLocalMediaDescription(d->localDesc);
+		d->getStreamsGroup().prepare();
 	}
 
+	d->op->setLocalMediaDescription(d->localDesc);
+
 	int result = CallSession::startInvite(destination, subject, content);
 	if (result < 0) {
 		if (d->state == CallSession::State::Error)
 			d->stopStreams();
 		return result;
 	}
-	if (getCore()->getCCore()->sip_conf.sdp_200_ack) {
-		// We are NOT offering, set local media description after sending the call so that we are ready to
-		// process the remote offer when it will arrive.
-		d->op->setLocalMediaDescription(d->localDesc);
-	}
 	return result;
 }
 
@@ -4933,21 +2334,20 @@ void MediaSession::startRecording () {
 		lError() << "MediaSession::startRecording(): no output file specified. Use MediaSessionParams::setRecordFilePath()";
 		return;
 	}
-	if (d->audioStream && !d->getParams()->getPrivate()->getInConference())
-		audio_stream_mixed_record_start(d->audioStream);
-	d->recordActive = true;
+	AudioControlInterface * i = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	i->startRecording();
 }
 
 void MediaSession::stopRecording () {
 	L_D();
-	if (d->audioStream && !d->getParams()->getPrivate()->getInConference())
-		audio_stream_mixed_record_stop(d->audioStream);
-	d->recordActive = false;
+	AudioControlInterface * i = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	i->stopRecording();
 }
 
 bool MediaSession::isRecording () {
 	L_D();
-	return d->recordActive;
+	AudioControlInterface * i = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	return i->isRecording();
 }
 
 void MediaSession::terminateBecauseOfLostMedia () {
@@ -4966,12 +2366,14 @@ LinphoneStatus MediaSession::update (const MediaSessionParams *msp, const string
 		return -1;
 	if (d->getCurrentParams() == msp)
 		lWarning() << "CallSession::update() is given the current params, this is probably not what you intend to do!";
-	d->iceAgent->checkSession(IR_Controlling, true);
 	if (msp) {
 		d->broken = false;
 		d->setState(nextState, "Updating call");
 		d->setParams(new MediaSessionParams(*msp));
-		if (d->iceAgent->prepare(d->localDesc, false)) {
+		if (!d->getParams()->getPrivate()->getNoUserConsent())
+			d->makeLocalMediaDescription(true);
+		
+		if (d->getStreamsGroup().prepare()) {
 			lInfo() << "Defer CallSession update to gather ICE candidates";
 			return 0;
 		}
@@ -4984,29 +2386,18 @@ LinphoneStatus MediaSession::update (const MediaSessionParams *msp, const string
 		const sound_config_t &soundConfig = getCore()->getCCore()->sound_conf;
 		const MSSndCard *captureCard = soundConfig.capt_sndcard;
 		const MSSndCard *playCard = soundConfig.lsd_card ? soundConfig.lsd_card : soundConfig.play_sndcard;
-
-		if (captureCard != d->currentCaptureCard || playCard != d->currentPlayCard) {
-			d->forceStreamsReconstruction = true;
+		
+		MS2AudioStream *as = d->getStreamsGroup().lookupMainStreamInterface<MS2AudioStream>(SalAudio);
+		if (as && ((captureCard != as->getCurrentCaptureCard()) || playCard != as->getCurrentPlaybackCard())) {
 			//Ideally this should use the same logic as video (See video_stream_change_camera)
 			//I.E. reconstruct only ms2 graphs without destroying the streams.
 			//For now, we just stop and restart audio stream with new playback/capture card
+			as->stop();
 			d->updateStreams(d->resultDesc, d->state);
+		}else{
+			VideoControlInterface *i = d->getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+			if (i) i->parametersChanged();
 		}
-
-	#ifdef VIDEO_ENABLED
-		else if (d->videoStream) {
-			const LinphoneVideoDefinition *vdef = linphone_core_get_preferred_video_definition(getCore()->getCCore());
-			MSVideoSize vsize;
-			vsize.width = static_cast<int>(linphone_video_definition_get_width(vdef));
-			vsize.height = static_cast<int>(linphone_video_definition_get_height(vdef));
-			video_stream_set_sent_video_size(d->videoStream, vsize);
-			video_stream_set_fps(d->videoStream, linphone_core_get_preferred_framerate(getCore()->getCCore()));
-			if (d->cameraEnabled && (d->videoStream->cam != getCore()->getCCore()->video_conf.device))
-				video_stream_change_camera(d->videoStream, getCore()->getCCore()->video_conf.device);
-			else
-				video_stream_update_video_params(d->videoStream);
-		}
-	#endif
 	}
 	return result;
 }
@@ -5015,52 +2406,22 @@ LinphoneStatus MediaSession::update (const MediaSessionParams *msp, const string
 
 void MediaSession::requestNotifyNextVideoFrameDecoded () {
 	L_D();
-	if (d->videoStream && d->videoStream->ms.decoder)
-		ms_filter_call_method_noarg(d->videoStream->ms.decoder, MS_VIDEO_DECODER_RESET_FIRST_IMAGE_NOTIFICATION);
-}
-
-void MediaSessionPrivate::snapshotTakenCb(void *userdata, struct _MSFilter *f, unsigned int id, void *arg) {
-#ifdef VIDEO_ENABLED
-	L_Q();
-	if (id == MS_JPEG_WRITER_SNAPSHOT_TAKEN) {
-		const char *filepath = (const char *) arg;
-		listener->onSnapshotTaken(q->getSharedFromThis(), filepath);
-	}
-#endif
+	VideoControlInterface *i = d->getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+	if (i) i->requestNotifyNextVideoFrameDecoded();
 }
 
-#ifdef VIDEO_ENABLED
-static void snapshot_taken(void *userdata, struct _MSFilter *f, unsigned int id, void *arg) {
-	MediaSessionPrivate *d = (MediaSessionPrivate *)userdata;
-	d->snapshotTakenCb(userdata, f, id, arg);
-}
-#endif
 
 LinphoneStatus MediaSession::takePreviewSnapshot (const string& file) {
-#ifdef VIDEO_ENABLED
 	L_D();
-	if (d->videoStream && d->videoStream->local_jpegwriter) {
-		ms_filter_clear_notify_callback(d->videoStream->jpegwriter);
-		const char *filepath = file.empty() ? nullptr : file.c_str();
-		ms_filter_add_notify_callback(d->videoStream->local_jpegwriter, snapshot_taken, d, TRUE);
-		return ms_filter_call_method(d->videoStream->local_jpegwriter, MS_JPEG_WRITER_TAKE_SNAPSHOT, (void *)filepath);
-	}
-	lWarning() << "Cannot take local snapshot: no currently running video stream on this call";
-#endif
+	VideoControlInterface *i = d->getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+	if (i) return i->takePreviewSnapshot(file);
 	return -1;
 }
 
 LinphoneStatus MediaSession::takeVideoSnapshot (const string& file) {
-#ifdef VIDEO_ENABLED
 	L_D();
-	if (d->videoStream && d->videoStream->jpegwriter) {
-		ms_filter_clear_notify_callback(d->videoStream->jpegwriter);
-		const char *filepath = file.empty() ? nullptr : file.c_str();
-		ms_filter_add_notify_callback(d->videoStream->jpegwriter, snapshot_taken, d, TRUE);
-		return ms_filter_call_method(d->videoStream->jpegwriter, MS_JPEG_WRITER_TAKE_SNAPSHOT, (void *)filepath);
-	}
-	lWarning() << "Cannot take snapshot: no currently running video stream on this call";
-#endif
+	VideoControlInterface *i = d->getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+	if (i) return i->takeVideoSnapshot(file);
 	return -1;
 }
 
@@ -5070,97 +2431,49 @@ void MediaSession::zoomVideo (float zoomFactor, float *cx, float *cy) {
 
 void MediaSession::zoomVideo (float zoomFactor, float cx, float cy) {
 	L_D();
-	if (d->videoStream && d->videoStream->output) {
-		if (zoomFactor < 1)
-			zoomFactor = 1;
-		float halfsize = 0.5f * 1.0f / zoomFactor;
-		if ((cx - halfsize) < 0)
-			cx = 0 + halfsize;
-		if ((cx + halfsize) > 1)
-			cx = 1 - halfsize;
-		if ((cy - halfsize) < 0)
-			cy = 0 + halfsize;
-		if ((cy + halfsize) > 1)
-			cy = 1 - halfsize;
-		float zoom[3] = { zoomFactor, cx, cy };
-		ms_filter_call_method(d->videoStream->output, MS_VIDEO_DISPLAY_ZOOM, &zoom);
-	} else
-		lWarning() << "Could not apply zoom: video output wasn't activated";
+	VideoControlInterface *i = d->getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+	if (i) i->zoomVideo(zoomFactor, cx, cy);
 }
 
 // -----------------------------------------------------------------------------
 
 bool MediaSession::cameraEnabled () const {
 	L_D();
-	return d->cameraEnabled;
+	VideoControlInterface *i = d->getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+	if (i) return i->cameraEnabled();
+	return false;
 }
 
-bool MediaSession::echoCancellationEnabled () const {
+void MediaSession::enableCamera (bool value) {
 	L_D();
-	if (!d->audioStream || !d->audioStream->ec)
-		return !!linphone_core_echo_cancellation_enabled(getCore()->getCCore());
-
-	bool val;
-	ms_filter_call_method(d->audioStream->ec, MS_ECHO_CANCELLER_GET_BYPASS_MODE, &val);
-	return !val;
+	VideoControlInterface *i = d->getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+	if (i) i->enableCamera(value);
 }
 
-bool MediaSession::echoLimiterEnabled () const {
+bool MediaSession::echoCancellationEnabled () const {
 	L_D();
-	if (d->audioStream)
-		return d->audioStream->el_type !=ELInactive;
-	return !!linphone_core_echo_limiter_enabled(getCore()->getCCore());
+	AudioControlInterface * i = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	return i ? i->echoCancellationEnabled() : false;
 }
 
-void MediaSession::enableCamera (bool value) {
-#ifdef VIDEO_ENABLED
+void MediaSession::enableEchoCancellation (bool value) {
 	L_D();
-	d->cameraEnabled = value;
-	switch (d->state) {
-		case CallSession::State::StreamsRunning:
-		case CallSession::State::OutgoingEarlyMedia:
-		case CallSession::State::IncomingEarlyMedia:
-		case CallSession::State::Connected:
-			if (d->videoStream && video_stream_started(d->videoStream) && (video_stream_get_camera(d->videoStream) != d->getVideoDevice())) {
-				string currentCam = video_stream_get_camera(d->videoStream) ? ms_web_cam_get_name(video_stream_get_camera(d->videoStream)) : "NULL";
-				string newCam = d->getVideoDevice() ? ms_web_cam_get_name(d->getVideoDevice()) : "NULL";
-				lInfo() << "Switching video cam from [" << currentCam << "] to [" << newCam << "] on CallSession [" << this << "]";
-				video_stream_change_camera(d->videoStream, d->getVideoDevice());
-			}
-			break;
-		default:
-			break;
-	}
-#endif
+	AudioControlInterface * i = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	if (i) i->enableEchoCancellation(value);
 }
 
-void MediaSession::enableEchoCancellation (bool value) {
-	L_D();
-	if (d->audioStream && d->audioStream->ec) {
-		bool bypassMode = !value;
-		ms_filter_call_method(d->audioStream->ec, MS_ECHO_CANCELLER_SET_BYPASS_MODE, &bypassMode);
-	}
+bool MediaSession::echoLimiterEnabled () const {
+	lWarning() << "MediaSession::echoLimiterEnabled() unimplemented.";
+	return false;
 }
 
 void MediaSession::enableEchoLimiter (bool value) {
-	L_D();
-	if (d->audioStream) {
-		if (value) {
-			string type = lp_config_get_string(linphone_core_get_config(getCore()->getCCore()), "sound", "el_type", "mic");
-			if (type == "mic")
-				audio_stream_enable_echo_limiter(d->audioStream, ELControlMic);
-			else if (type == "full")
-				audio_stream_enable_echo_limiter(d->audioStream, ELControlFull);
-		} else
-			audio_stream_enable_echo_limiter(d->audioStream, ELInactive);
-	}
+	lWarning() << "MediaSession::enableEchoLimiter() unimplemented.";
 }
 
 bool MediaSession::getAllMuted () const {
 	L_D();
-	if (d->audioStream && d->videoStream) return d->audioMuted && d->videoMuted;
-	if (d->audioStream) return d->audioMuted;
-	return d->videoMuted;
+	return d->getStreamsGroup().isMuted();
 }
 
 LinphoneCallStats * MediaSession::getAudioStats () const {
@@ -5169,23 +2482,17 @@ LinphoneCallStats * MediaSession::getAudioStats () const {
 
 string MediaSession::getAuthenticationToken () const {
 	L_D();
-	return d->authToken;
+	return d->getStreamsGroup().getAuthenticationToken();
 }
 
 bool MediaSession::getAuthenticationTokenVerified () const {
 	L_D();
-	return d->authTokenVerified;
+	return d->getStreamsGroup().getAuthenticationTokenVerified();
 }
 
 float MediaSession::getAverageQuality () const {
 	L_D();
-	float audioRating = -1.f;
-	float videoRating = -1.f;
-	if (d->audioStream)
-		audioRating = media_stream_get_average_quality_rating(&d->audioStream->ms) / 5.0f;
-	if (d->videoStream)
-		videoRating = media_stream_get_average_quality_rating(&d->videoStream->ms) / 5.0f;
-	return MediaSessionPrivate::aggregateQualityRatings(audioRating, videoRating);
+	return d->getStreamsGroup().getAverageQuality();
 }
 
 MediaSessionParams * MediaSession::getCurrentParams () const {
@@ -5196,13 +2503,7 @@ MediaSessionParams * MediaSession::getCurrentParams () const {
 
 float MediaSession::getCurrentQuality () const {
 	L_D();
-	float audioRating = -1.f;
-	float videoRating = -1.f;
-	if (d->audioStream)
-		audioRating = media_stream_get_quality_rating(&d->audioStream->ms) / 5.0f;
-	if (d->videoStream)
-		videoRating = media_stream_get_quality_rating(&d->videoStream->ms) / 5.0f;
-	return MediaSessionPrivate::aggregateQualityRatings(audioRating, videoRating);
+	return d->getStreamsGroup().getCurrentQuality();
 }
 
 const MediaSessionParams * MediaSession::getMediaParams () const {
@@ -5212,46 +2513,95 @@ const MediaSessionParams * MediaSession::getMediaParams () const {
 
 RtpTransport * MediaSession::getMetaRtcpTransport (int streamIndex) const {
 	L_D();
-	if ((streamIndex < 0) || (streamIndex >= getStreamCount()))
+	MS2Stream *s = dynamic_cast<MS2Stream*>(d->getStreamsGroup().getStream(streamIndex));
+	if (!s){
+		lError() << "MediaSession::getMetaRtcpTransport(): no stream with index " << streamIndex;
 		return nullptr;
-	RtpTransport *metaRtp;
-	RtpTransport *metaRtcp;
-	rtp_session_get_transports(d->sessions[streamIndex].rtp_session, &metaRtp, &metaRtcp);
-	return metaRtcp;
+	}
+	return s->getMetaRtpTransports().second;
 }
 
 RtpTransport * MediaSession::getMetaRtpTransport (int streamIndex) const {
 	L_D();
-	if ((streamIndex < 0) || (streamIndex >= getStreamCount()))
+	MS2Stream *s = dynamic_cast<MS2Stream*>(d->getStreamsGroup().getStream(streamIndex));
+	if (!s){
+		lError() << "MediaSession::getMetaRtcpTransport(): no stream with index " << streamIndex;
 		return nullptr;
-	RtpTransport *metaRtp;
-	RtpTransport *metaRtcp;
-	rtp_session_get_transports(d->sessions[streamIndex].rtp_session, &metaRtp, &metaRtcp);
-	return metaRtp;
+	}
+	return s->getMetaRtpTransports().first;
 }
 
 float MediaSession::getMicrophoneVolumeGain () const {
 	L_D();
-	if (d->audioStream)
-		return audio_stream_get_sound_card_input_gain(d->audioStream);
-	else {
+	AudioControlInterface *iface = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	if (iface){
+		return iface->getMicGain();
+	} else {
 		lError() << "Could not get record volume: no audio stream";
 		return -1.0f;
 	}
 }
 
+void MediaSession::setMicrophoneVolumeGain (float value) {
+	L_D();
+	AudioControlInterface *iface = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	if (iface)
+		iface->setMicGain(value);
+	else
+		lError() << "Could not set record volume: no audio stream";
+}
+
+float MediaSession::getSpeakerVolumeGain () const {
+	L_D();
+	AudioControlInterface *iface = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	if (iface)
+		return iface->getSpeakerGain();
+	else {
+		lError() << "Could not get playback volume: no audio stream";
+		return -1.0f;
+	}
+}
+
+void MediaSession::setSpeakerVolumeGain (float value) {
+	L_D();
+	AudioControlInterface *iface = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	if (iface)
+		iface->setSpeakerGain(value);
+	else
+		lError() << "Could not set playback volume: no audio stream";
+}
+
 void * MediaSession::getNativeVideoWindowId () const {
 	L_D();
-	if (d->videoWindowId) {
-		/* The video id was previously set by the app */
-		return d->videoWindowId;
+	auto iface = d->getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+	if (iface) {
+		iface->getNativeWindowId();
 	}
-#ifdef VIDEO_ENABLED
-	else if (d->videoStream) {
-		/* It was not set but we want to get the one automatically created by mediastreamer2 (desktop versions only) */
-		return video_stream_get_native_window_id(d->videoStream);
+	return nullptr;
+}
+
+void MediaSession::setNativeVideoWindowId (void *id) {
+	L_D();
+	auto iface = d->getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+	if (iface) {
+		iface->setNativeWindowId(id);
+	}
+}
+
+void MediaSession::setNativePreviewWindowId(void *id){
+	L_D();
+	auto iface = d->getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+	if (iface) {
+		iface->setNativePreviewWindowId(id);
+	}
+}
+
+void * MediaSession::getNativePreviewVideoWindowId () const{
+	L_D();
+	auto iface = d->getStreamsGroup().lookupMainStreamInterface<VideoControlInterface>(SalVideo);
+	if (iface) {
+		iface->getNativePreviewWindowId();
 	}
-#endif
 	return nullptr;
 }
 
@@ -5262,20 +2612,17 @@ const CallSessionParams * MediaSession::getParams () const {
 
 float MediaSession::getPlayVolume () const {
 	L_D();
-	if (d->audioStream && d->audioStream->volrecv) {
-		float vol = 0;
-		ms_filter_call_method(d->audioStream->volrecv, MS_VOLUME_GET, &vol);
-		return vol;
-	}
+	AudioControlInterface *iface = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	if (iface) return iface->getPlayVolume();
 	return LINPHONE_VOLUME_DB_LOWEST;
 }
 
 float MediaSession::getRecordVolume () const {
 	L_D();
-	if (d->audioStream && d->audioStream->volsend && !d->microphoneMuted && (d->state == CallSession::State::StreamsRunning)) {
-		float vol = 0;
-		ms_filter_call_method(d->audioStream->volsend, MS_VOLUME_GET, &vol);
-		return vol;
+	
+	if (d->state == CallSession::State::StreamsRunning){
+		AudioControlInterface *iface = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+		if (iface) return iface->getRecordVolume();
 	}
 	return LINPHONE_VOLUME_DB_LOWEST;
 }
@@ -5284,109 +2631,90 @@ const MediaSessionParams * MediaSession::getRemoteParams () {
 	L_D();
 	if (d->op){
 		SalMediaDescription *md = d->op->getRemoteMediaDescription();
+		MediaSessionParams * params = nullptr;
 		if (md) {
-			d->setRemoteParams(new MediaSessionParams());
-			unsigned int nbAudioStreams = sal_media_description_nb_active_streams_of_type(md, SalAudio);
-			for (unsigned int i = 0; i < nbAudioStreams; i++) {
-				SalStreamDescription *sd = sal_media_description_get_active_stream_of_type(md, SalAudio, i);
-				if (sal_stream_description_has_srtp(sd))
-					d->getRemoteParams()->setMediaEncryption(LinphoneMediaEncryptionSRTP);
-			}
-			unsigned int nbVideoStreams = sal_media_description_nb_active_streams_of_type(md, SalVideo);
-			for (unsigned int i = 0; i < nbVideoStreams; i++) {
-				SalStreamDescription *sd = sal_media_description_get_active_stream_of_type(md, SalVideo, i);
-				if (sal_stream_description_active(sd))
-					d->getRemoteParams()->enableVideo(true);
-				if (sal_stream_description_has_srtp(sd))
-					d->getRemoteParams()->setMediaEncryption(LinphoneMediaEncryptionSRTP);
-			}
-			unsigned int nbTextStreams = sal_media_description_nb_active_streams_of_type(md, SalText);
-			for (unsigned int i = 0; i < nbTextStreams; i++) {
-				SalStreamDescription *sd = sal_media_description_get_active_stream_of_type(md, SalText, i);
-				if (sal_stream_description_has_srtp(sd))
-					d->getRemoteParams()->setMediaEncryption(LinphoneMediaEncryptionSRTP);
-				d->getRemoteParams()->enableRealtimeText(true);
-			}
-			if (!d->getRemoteParams()->videoEnabled()) {
+			SalStreamDescription *sd;
+			params = new MediaSessionParams();
+			
+			if (d->mainAudioStreamIndex != -1 && d->mainAudioStreamIndex < md->nb_streams){
+				sd = &md->streams[d->mainAudioStreamIndex];
+				params->enableAudio(sal_stream_description_enabled(sd));
+				params->setMediaEncryption(sal_stream_description_has_srtp(sd) ? LinphoneMediaEncryptionSRTP : LinphoneMediaEncryptionNone);
+				params->getPrivate()->setCustomSdpMediaAttributes(LinphoneStreamTypeAudio, md->streams[d->mainAudioStreamIndex].custom_sdp_attributes);
+			}else params->enableAudio(false);
+			
+			if (d->mainVideoStreamIndex != -1 && d->mainVideoStreamIndex < md->nb_streams){
+				sd = &md->streams[d->mainVideoStreamIndex];
+				params->enableVideo(sal_stream_description_enabled(sd));
+				params->setMediaEncryption(sal_stream_description_has_srtp(sd) ? LinphoneMediaEncryptionSRTP : LinphoneMediaEncryptionNone);
+				params->getPrivate()->setCustomSdpMediaAttributes(LinphoneStreamTypeVideo, md->streams[d->mainVideoStreamIndex].custom_sdp_attributes);
+			}else params->enableVideo(false);
+			
+			if (d->mainTextStreamIndex != -1 && d->mainTextStreamIndex < md->nb_streams){
+				sd = &md->streams[d->mainTextStreamIndex];
+				params->enableRealtimeText(sal_stream_description_enabled(sd));
+				params->setMediaEncryption(sal_stream_description_has_srtp(sd) ? LinphoneMediaEncryptionSRTP : LinphoneMediaEncryptionNone);
+				params->getPrivate()->setCustomSdpMediaAttributes(LinphoneStreamTypeText, md->streams[d->mainTextStreamIndex].custom_sdp_attributes);
+			}else params->enableRealtimeText(false);
+			
+			if (!params->videoEnabled()) {
 				if ((md->bandwidth > 0) && (md->bandwidth <= linphone_core_get_edge_bw(getCore()->getCCore())))
-					d->getRemoteParams()->enableLowBandwidth(true);
+					params->enableLowBandwidth(true);
 			}
 			if (md->name[0] != '\0')
-				d->getRemoteParams()->setSessionName(md->name);
-
-			d->getRemoteParams()->getPrivate()->setCustomSdpAttributes(md->custom_sdp_attributes);
-			d->getRemoteParams()->getPrivate()->setCustomSdpMediaAttributes(LinphoneStreamTypeAudio, md->streams[d->mainAudioStreamIndex].custom_sdp_attributes);
-			d->getRemoteParams()->getPrivate()->setCustomSdpMediaAttributes(LinphoneStreamTypeVideo, md->streams[d->mainVideoStreamIndex].custom_sdp_attributes);
-			d->getRemoteParams()->getPrivate()->setCustomSdpMediaAttributes(LinphoneStreamTypeText, md->streams[d->mainTextStreamIndex].custom_sdp_attributes);
+				params->setSessionName(md->name);
+			params->getPrivate()->setCustomSdpAttributes(md->custom_sdp_attributes);
+			params->enableRtpBundle(md->bundles != nullptr);
 		}
 		const SalCustomHeader *ch = d->op->getRecvCustomHeaders();
 		if (ch) {
-			/* Instanciate a remote_params only if a SIP message was received before (custom headers indicates this) */
-			if (!d->remoteParams)
-				d->setRemoteParams(new MediaSessionParams());
-			d->getRemoteParams()->getPrivate()->setCustomHeaders(ch);
+			if (!params) params = new MediaSessionParams();
+			params->getPrivate()->setCustomHeaders(ch);
 		}
-
-		const list<Content> additionnalContents = d->op->getAdditionalRemoteBodies();
-		for (auto& content : additionnalContents)
-			d->remoteParams->addCustomContent(content);
-			
-		return d->getRemoteParams();
+		const list<Content> &additionnalContents = d->op->getAdditionalRemoteBodies();
+		for (auto& content : additionnalContents){
+			if (!params) params = new MediaSessionParams();
+			params->addCustomContent(content);
+		}
+		d->setRemoteParams(params);
+		return params;
 	}
 	return nullptr;
 }
 
-float MediaSession::getSpeakerVolumeGain () const {
-	L_D();
-	if (d->audioStream)
-		return audio_stream_get_sound_card_output_gain(d->audioStream);
-	else {
-		lError() << "Could not get playback volume: no audio stream";
-		return -1.0f;
-	}
-}
-
 LinphoneCallStats * MediaSession::getStats (LinphoneStreamType type) const {
 	L_D();
 	if (type == LinphoneStreamTypeUnknown)
 		return nullptr;
 	LinphoneCallStats *stats = nullptr;
-	LinphoneCallStats *statsCopy = _linphone_call_stats_new();
-	if (type == LinphoneStreamTypeAudio)
-		stats = d->audioStats;
-	else if (type == LinphoneStreamTypeVideo)
-		stats = d->videoStats;
-	else if (type == LinphoneStreamTypeText)
-		stats = d->textStats;
-	MediaStream *ms = d->getMediaStream(type);
-	if (ms && stats)
-		linphone_call_stats_update(stats, ms);
-	_linphone_call_stats_clone(statsCopy, stats);
+	LinphoneCallStats *statsCopy = nullptr;
+	Stream *s = d->getStream(type);
+	if (s && (stats = s->getStats())) {
+		statsCopy = (LinphoneCallStats*) belle_sip_object_clone((belle_sip_object_t*)stats);
+	}
 	return statsCopy;
 }
 
 int MediaSession::getStreamCount () const {
-	/* TODO: Revisit when multiple media streams will be implemented */
-#ifdef VIDEO_ENABLED
-	if (getCurrentParams()->realtimeTextEnabled())
-		return 3;
-	return 2;
-#else
-	if (getCurrentParams()->realtimeTextEnabled())
-		return 2;
-	return 1;
-#endif
+	L_D();
+	return (int)d->getStreamsGroup().size();
 }
 
 MSFormatType MediaSession::getStreamType (int streamIndex) const {
 	L_D();
-	/* TODO: Revisit when multiple media streams will be implemented */
-	if (streamIndex == d->mainVideoStreamIndex)
-		return MSVideo;
-	else if (streamIndex == d->mainTextStreamIndex)
-		return MSText;
-	else if (streamIndex == d->mainAudioStreamIndex)
-		return MSAudio;
+	Stream *s = d->getStreamsGroup().getStream(streamIndex);
+	if (s){
+		switch(s->getType()){
+			case SalAudio:
+				return MSAudio;
+			case SalVideo:
+				return MSVideo;
+			case SalText:
+				return MSText;
+			case SalOther:
+			break;
+		}
+	}
 	return MSUnknownMedia;
 }
 
@@ -5400,35 +2728,25 @@ LinphoneCallStats * MediaSession::getVideoStats () const {
 
 bool MediaSession::mediaInProgress () const {
 	L_D();
-	if ((linphone_call_stats_get_ice_state(d->audioStats) == LinphoneIceStateInProgress)
-		|| (linphone_call_stats_get_ice_state(d->videoStats) == LinphoneIceStateInProgress)
-		|| (linphone_call_stats_get_ice_state(d->textStats) == LinphoneIceStateInProgress))
-		return true;
-	/* TODO: could check zrtp state */
+	for(auto &stream : d->getStreamsGroup().getStreams()){
+		LinphoneCallStats *stats = stream->getStats();
+		if (stats && linphone_call_stats_get_ice_state(stats) == LinphoneIceStateInProgress){
+			return true;
+		}
+	}
 	return false;
 }
 
 void MediaSession::setAudioRoute (LinphoneAudioRoute route) {
 	L_D();
-	if (d->audioStream)
-		audio_stream_set_audio_route(d->audioStream, (MSAudioRoute)route);
+	AudioControlInterface *i = d->getStreamsGroup().lookupMainStreamInterface<AudioControlInterface>(SalAudio);
+	if (i) i->setRoute(route);
 }
 
 void MediaSession::setAuthenticationTokenVerified (bool value) {
 	L_D();
-	if (!d->audioStream || !media_stream_started(&d->audioStream->ms)) {
-		lError() << "MediaSession::setAuthenticationTokenVerified(): No audio stream or not started";
-		return;
-	}
-	if (!d->audioStream->ms.sessions.zrtp_context) {
-		lError() << "MediaSession::setAuthenticationTokenVerified(): No zrtp context";
-		return;
-	}
-	// SAS verified
-	if (value) {
-		ms_zrtp_sas_verified(d->audioStream->ms.sessions.zrtp_context);
-	} else { // SAS rejected
-		ms_zrtp_sas_reset_verified(d->audioStream->ms.sessions.zrtp_context);
+	d->getStreamsGroup().setAuthTokenVerified(value);
+	if (!value) {
 		char *peerDeviceId = nullptr;
 		auto encryptionEngine = getCore()->getEncryptionEngine();
 		if (encryptionEngine) { //inform lime that zrtp no longuer guaranty the trust
@@ -5438,41 +2756,26 @@ void MediaSession::setAuthenticationTokenVerified (bool value) {
 			ms_free(peerDeviceId);
 		}
 	}
-	d->authTokenVerified = value;
 	d->propagateEncryptionChanged();
 }
 
-void MediaSession::setMicrophoneVolumeGain (float value) {
-	L_D();
-	if(d->audioStream)
-		audio_stream_set_sound_card_input_gain(d->audioStream, value);
-	else
-		lError() << "Could not set record volume: no audio stream";
-}
-
-void MediaSession::setNativeVideoWindowId (void *id) {
-	L_D();
-	d->videoWindowId = id;
-#ifdef VIDEO_ENABLED
-	if (d->videoStream)
-		video_stream_set_native_window_id(d->videoStream, id);
-#endif
-}
-
 void MediaSession::setParams (const MediaSessionParams *msp) {
 	L_D();
-	if ((d->state == CallSession::State::OutgoingInit) || (d->state == CallSession::State::IncomingReceived))
-		d->setParams(msp ? new MediaSessionParams(*msp) : nullptr);
-	else
-		lError() << "MediaSession::setParams(): Invalid state %s", Utils::toString(d->state);
+		
+	switch(d->state){
+		case CallSession::State::OutgoingInit:
+		case CallSession::State::IncomingReceived:
+			d->setParams(msp ? new MediaSessionParams(*msp) : nullptr);
+			// Update the local media description.
+			d->makeLocalMediaDescription(d->state == CallSession::State::OutgoingInit ? 
+				!getCore()->getCCore()->sip_conf.sdp_200_ack : false);
+		break;
+		default:
+			lError() << "MediaSession::setParams(): Invalid state %s", Utils::toString(d->state);
+		break;
+	}
 }
 
-void MediaSession::setSpeakerVolumeGain (float value) {
-	L_D();
-	if (d->audioStream)
-		audio_stream_set_sound_card_output_gain(d->audioStream, value);
-	else
-		lError() << "Could not set playback volume: no audio stream";
-}
+
 
 LINPHONE_END_NAMESPACE
diff --git a/src/conference/session/media-session.h b/src/conference/session/media-session.h
index ff39c48c1ec89e13cfa4929cc88d415984102301..52f465ead44324e2feec076ed10980cdae6e7ee2 100644
--- a/src/conference/session/media-session.h
+++ b/src/conference/session/media-session.h
@@ -38,6 +38,8 @@ class LINPHONE_PUBLIC MediaSession : public CallSession {
 	friend class CallPrivate;
 	friend class IceAgent;
 	friend class ToneManager;
+	friend class Stream;
+	friend class StreamsGroup;
 
 public:
 	MediaSession (const std::shared_ptr<Core> &core, std::shared_ptr<Participant> me, const CallSessionParams *params, CallSessionListener *listener);
@@ -90,6 +92,7 @@ public:
 	RtpTransport * getMetaRtpTransport (int streamIndex) const;
 	float getMicrophoneVolumeGain () const;
 	void * getNativeVideoWindowId () const;
+	void * getNativePreviewVideoWindowId () const;
 	const CallSessionParams *getParams () const override;
 	float getPlayVolume () const;
 	float getRecordVolume () const;
@@ -105,9 +108,9 @@ public:
 	void setAuthenticationTokenVerified (bool value);
 	void setMicrophoneVolumeGain (float value);
 	void setNativeVideoWindowId (void *id);
+	void setNativePreviewWindowId (void *id);
 	void setParams (const MediaSessionParams *msp);
 	void setSpeakerVolumeGain (float value);
-
 private:
 	L_DECLARE_PRIVATE(MediaSession);
 	L_DISABLE_COPY(MediaSession);
diff --git a/src/conference/session/ms2-stream.cpp b/src/conference/session/ms2-stream.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2fbac7d2afb059e78558cf9fb2f9a161bf407c8d
--- /dev/null
+++ b/src/conference/session/ms2-stream.cpp
@@ -0,0 +1,1104 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <bctoolbox/defs.h>
+
+#include "ms2-streams.h"
+#include "media-session.h"
+#include "media-session-p.h"
+#include "core/core.h"
+#include "c-wrapper/c-wrapper.h"
+#include "call/call.h"
+#include "call/call-p.h"
+#include "conference/participant.h"
+#include "utils/payload-type-handler.h"
+#include "conference/params/media-session-params-p.h"
+#include "nat/ice-service.h"
+
+#include "linphone/core.h"
+
+
+using namespace::std;
+
+LINPHONE_BEGIN_NAMESPACE
+
+/*
+ * MS2Stream implementation
+ */
+
+MS2Stream::MS2Stream(StreamsGroup &sg, const OfferAnswerContext &params) : Stream(sg, params){
+	memset(&mSessions, 0, sizeof(mSessions));
+	initMulticast(params);
+	mStats = _linphone_call_stats_new();
+	_linphone_call_stats_set_type(mStats, (LinphoneStreamType)getType());
+	_linphone_call_stats_set_received_rtcp(mStats, nullptr);
+	_linphone_call_stats_set_sent_rtcp(mStats, nullptr);
+	_linphone_call_stats_set_ice_state(mStats, LinphoneIceStateNotActivated);
+}
+
+void MS2Stream::removeFromBundle(){
+	if (mRtpBundle){
+		rtp_bundle_remove_session(mRtpBundle, mSessions.rtp_session);
+		if (mOwnsBundle){
+			RtpBundle *bundle = mRtpBundle;
+			getGroup().addPostRenderHook( [bundle](){
+				rtp_bundle_delete(bundle);
+			});
+			mOwnsBundle = false;
+			getMediaSessionPrivate().getCurrentParams()->enableRtpBundle(false);
+		}
+		mRtpBundle = nullptr;
+		mBundleOwner = nullptr;
+	}
+}
+
+void MS2Stream::initRtpBundle(const OfferAnswerContext &params){
+	int index = sal_media_description_get_index_of_transport_owner(params.resultMediaDescription, params.resultStreamDescription);
+	if (index == -1) {
+		lInfo() << *this << " is not part of any bundle";
+		removeFromBundle();
+		return ; /*No bundle to handle */
+	}
+		
+	mBundleOwner = dynamic_cast<MS2Stream*>(getGroup().getStream((size_t)index));
+	if (!mBundleOwner){
+		lError() << "Could not locate the stream owning the bundle's transport.";
+		removeFromBundle();
+		return;
+	}
+	RtpBundle * bundle = mBundleOwner->createOrGetRtpBundle(params.resultStreamDescription);
+	if (bundle && mBundleOwner != this && mRtpBundle == nullptr){
+		lInfo() << "Stream " << *this << " added to rtp bundle " << bundle << " with mid '" << params.resultStreamDescription->mid << "'";
+		rtp_bundle_add_session(bundle, params.resultStreamDescription->mid, mSessions.rtp_session);
+		mRtpBundle = bundle;
+		mOwnsBundle = false;
+		getMediaSessionPrivate().getCurrentParams()->enableRtpBundle(true);
+	}
+
+	// It is necessary to call this function after adding the session to the bundle so the SDES contains the MID item
+	string userAgent = linphone_core_get_user_agent(getCCore());
+	rtp_session_set_source_description(mSessions.rtp_session, getMediaSessionPrivate().getMe()->getAddress().asString().c_str(), NULL, NULL, NULL, NULL, userAgent.c_str(), NULL);
+}
+
+RtpBundle *MS2Stream::createOrGetRtpBundle(const SalStreamDescription *sd){
+	if (!mRtpBundle){
+		mRtpBundle = rtp_bundle_new();
+		lInfo() << "Stream " << *this << " with mid '" << sd->mid << "'is the owner of rtp bundle " << mRtpBundle;
+		rtp_bundle_add_session(mRtpBundle, sd->mid, mSessions.rtp_session);
+		rtp_bundle_set_mid_extension_id(mRtpBundle, sd->mid_rtp_ext_header_id);
+		mOwnsBundle = true;
+		getMediaSessionPrivate().getCurrentParams()->enableRtpBundle(true);
+	}
+	return mRtpBundle;
+}
+
+void MS2Stream::setIceCheckList(IceCheckList *cl){
+	mIceCheckList = cl;
+	MediaStream *stream = getMediaStream();
+	if (stream){
+		rtp_session_set_pktinfo(mSessions.rtp_session, cl != nullptr);
+		rtp_session_set_symmetric_rtp(mSessions.rtp_session, (cl == nullptr) ? linphone_core_symmetric_rtp_enabled(getCCore()) : false);
+		media_stream_set_ice_check_list(stream, cl);
+	}
+	if (!cl){
+		updateIceInStats();
+	}
+}
+
+string MS2Stream::getBindIp(){
+	string bindIp = lp_config_get_string(linphone_core_get_config(getCCore()), "rtp", "bind_address", "");
+	
+	if (!mPortConfig.multicastIp.empty()){
+		if (mPortConfig.multicastRole == SalMulticastSender) {
+			/* As multicast sender, we must decide a local interface to use to send multicast, and bind to it */
+			char multicastBindIp[LINPHONE_IPADDR_SIZE] = {0};
+			linphone_core_get_local_ip_for((mPortConfig.multicastIp.find_first_of(':') == string::npos) ? AF_INET : AF_INET6, nullptr, multicastBindIp);
+			bindIp = mPortConfig.multicastBindIp = multicastBindIp;
+		} else {
+			/* Otherwise we shall use an address family of the same family of the multicast address, because
+			 * dual stack socket and multicast don't work well on Mac OS (linux is OK, as usual). */
+			bindIp = (mPortConfig.multicastIp.find_first_of(':') == string::npos) ? "0.0.0.0" : "::0";
+		}
+	}else if (bindIp.empty()){
+		/*If ipv6 is not enabled, listen to 0.0.0.0. The default behavior of mediastreamer when no IP is passed is to try ::0, and in
+		 * case of failure try 0.0.0.0 . But we don't want this if IPv6 is explicitely disabled.*/
+		if (!linphone_core_ipv6_enabled(getCCore())){
+			bindIp = "0.0.0.0";
+		}
+	}
+	return bindIp;
+}
+
+void MS2Stream::fillLocalMediaDescription(OfferAnswerContext & ctx){
+	SalStreamDescription *localDesc = ctx.localStreamDescription;
+	strncpy(localDesc->rtp_addr, getPublicIp().c_str(), sizeof(localDesc->rtp_addr) - 1);
+	strncpy(localDesc->rtcp_addr, getPublicIp().c_str(), sizeof(localDesc->rtcp_addr) -1);
+	
+	if (localDesc->rtp_port == SAL_STREAM_DESCRIPTION_PORT_TO_BE_DETERMINED && localDesc->payloads != nullptr){
+		/* Don't fill ports if no codecs are defined. The stream is not valid and should be disabled.*/
+		localDesc->rtp_port = mPortConfig.rtpPort;
+		localDesc->rtcp_port = mPortConfig.rtcpPort;
+	}
+	if (!isTransportOwner()){
+		/* A secondary stream part of a bundle must set port to zero and add the bundle-only attribute. */
+		localDesc->rtp_port = 0;
+		localDesc->bundle_only = TRUE;
+	}
+	
+	localDesc->rtp_ssrc = rtp_session_get_send_ssrc(mSessions.rtp_session);
+	
+	if (linphone_core_media_encryption_supported(getCCore(), LinphoneMediaEncryptionZRTP)) {
+		/* set the hello hash */
+		if (mSessions.zrtp_context) {
+			ms_zrtp_getHelloHash(mSessions.zrtp_context, localDesc->zrtphash, 128);
+			/* Turn on the flag to use it if ZRTP is set */
+			localDesc->haveZrtpHash = (getMediaSessionPrivate().getParams()->getMediaEncryption() == LinphoneMediaEncryptionZRTP);
+		} else
+			localDesc->haveZrtpHash = 0;
+	}
+	if (sal_stream_description_has_dtls(localDesc)) {
+		/* Get the self fingerprint from call (it's computed at stream init) */
+		strncpy(localDesc->dtls_fingerprint, mDtlsFingerPrint.c_str(), sizeof(localDesc->dtls_fingerprint) - 1);
+		/* If we are offering, SDP will have actpass setup attribute when role is unset, if we are responding the result mediadescription will be set to SalDtlsRoleIsClient */
+		localDesc->dtls_role = SalDtlsRoleUnset;
+	} else {
+		localDesc->dtls_fingerprint[0] = '\0';
+		localDesc->dtls_role = SalDtlsRoleInvalid;
+	}
+	/* In case we were offered multicast, we become multicast receiver. The local media description must reflect this. */
+	localDesc->multicast_role = mPortConfig.multicastRole;
+	Stream::fillLocalMediaDescription(ctx);
+}
+
+void MS2Stream::refreshSockets(){
+	rtp_session_refresh_sockets(mSessions.rtp_session);
+}
+
+void MS2Stream::initMulticast(const OfferAnswerContext &params) {
+	mPortConfig.multicastRole = params.localStreamDescription->multicast_role;
+	
+	if (mPortConfig.multicastRole == SalMulticastReceiver){
+		mPortConfig.multicastIp = params.remoteStreamDescription->rtp_addr;
+		mPortConfig.rtpPort = params.remoteStreamDescription->rtp_port;
+		mPortConfig.rtcpPort = 0; /*RTCP deactivated in multicast*/
+	}
+	
+	lInfo() << *this << ": multicast role is ["
+		<< sal_multicast_role_to_string(mPortConfig.multicastRole) << "]";
+}
+
+void MS2Stream::configureRtpSessionForRtcpFb (const OfferAnswerContext &params) {
+	if (getType() != SalAudio && getType() != SalVideo) return; //No AVPF for other than audio/video
+	
+	rtp_session_enable_avpf_feature(mSessions.rtp_session, ORTP_AVPF_FEATURE_GENERIC_NACK, !!params.resultStreamDescription->rtcp_fb.generic_nack_enabled);
+	rtp_session_enable_avpf_feature(mSessions.rtp_session, ORTP_AVPF_FEATURE_TMMBR, !!params.resultStreamDescription->rtcp_fb.tmmbr_enabled);
+}
+
+void MS2Stream::configureRtpSessionForRtcpXr(const OfferAnswerContext &params) {
+	OrtpRtcpXrConfiguration currentConfig;
+	const OrtpRtcpXrConfiguration *remoteConfig = &params.remoteStreamDescription->rtcp_xr;
+	if (params.localStreamDescription->dir == SalStreamInactive)
+		return;
+	else if (params.localStreamDescription->dir == SalStreamRecvOnly) {
+		/* Use local config for unilateral parameters and remote config for collaborative parameters */
+		memcpy(&currentConfig, &params.localStreamDescription->rtcp_xr, sizeof(currentConfig));
+		currentConfig.rcvr_rtt_mode = remoteConfig->rcvr_rtt_mode;
+		currentConfig.rcvr_rtt_max_size = remoteConfig->rcvr_rtt_max_size;
+	} else
+		memcpy(&currentConfig, remoteConfig, sizeof(currentConfig));
+	
+	rtp_session_configure_rtcp_xr(mSessions.rtp_session, &currentConfig);
+}
+
+void MS2Stream::configureAdaptiveRateControl (const OfferAnswerContext &params) {
+	if (getState() == Stream::Running){
+		return; // If stream is already running, these things are not expected to change.
+	}
+	bool videoWillBeUsed = false;
+	MediaStream *ms = getMediaStream();
+	const SalStreamDescription *vstream = sal_media_description_find_best_stream(const_cast<SalMediaDescription*>(params.resultMediaDescription), SalVideo);
+	if (vstream && (vstream->dir != SalStreamInactive) && vstream->payloads) {
+		/* When video is used, do not make adaptive rate control on audio, it is stupid */
+		videoWillBeUsed = true;
+	}
+	bool enabled = !!linphone_core_adaptive_rate_control_enabled(getCCore());
+	if (!enabled) {
+		media_stream_enable_adaptive_bitrate_control(ms, false);
+		return;
+	}
+	bool isAdvanced = true;
+	string algo = linphone_core_get_adaptive_rate_algorithm(getCCore());
+	if (algo == "basic")
+		isAdvanced = false;
+	else if (algo == "advanced")
+		isAdvanced = true;
+	
+	if (isAdvanced && !params.resultStreamDescription->rtcp_fb.tmmbr_enabled) {
+		lWarning() << "Advanced adaptive rate control requested but avpf-tmmbr is not activated in this stream. Reverting to basic rate control instead";
+		isAdvanced = false;
+	}
+	if (isAdvanced) {
+		lInfo() << "Setting up advanced rate control";
+		ms_bandwidth_controller_add_stream(getCCore()->bw_controller, ms);
+		media_stream_enable_adaptive_bitrate_control(ms, false);
+	} else {
+		media_stream_set_adaptive_bitrate_algorithm(ms, MSQosAnalyzerAlgorithmSimple);
+		if (getType() == SalAudio && videoWillBeUsed) {
+			/* If this is an audio stream but video is going to be used, there is no need to perform
+			 * basic rate control on the audio stream, just the video stream. */
+			enabled = false;
+		}
+		media_stream_enable_adaptive_bitrate_control(ms, enabled);
+	}
+}
+
+void MS2Stream::tryEarlyMediaForking(const OfferAnswerContext &ctx){
+	RtpSession *session = mSessions.rtp_session;
+	const SalStreamDescription *newStream = ctx.remoteStreamDescription;
+	const char *rtpAddr = (newStream->rtp_addr[0] != '\0') ? newStream->rtp_addr : ctx.remoteMediaDescription->addr;
+	const char *rtcpAddr = (newStream->rtcp_addr[0] != '\0') ? newStream->rtcp_addr : ctx.remoteMediaDescription->addr;
+	if (!ms_is_multicast(rtpAddr)){
+		rtp_session_set_symmetric_rtp(session, false); // Disable symmetric RTP when auxiliary destinations are added.
+		rtp_session_add_aux_remote_addr_full(session, rtpAddr, newStream->rtp_port, rtcpAddr, newStream->rtcp_port);
+		mUseAuxDestinations = true;
+	}
+	Stream::tryEarlyMediaForking(ctx);
+}
+
+void MS2Stream::finishEarlyMediaForking(){
+	if (mUseAuxDestinations){
+		rtp_session_set_symmetric_rtp(mSessions.rtp_session, linphone_core_symmetric_rtp_enabled(getCCore()));
+		rtp_session_clear_aux_remote_addr(mSessions.rtp_session);
+		mUseAuxDestinations = false;
+	}
+}
+
+/* 
+ * This function is used by derived implementations that need to extract the destination of RTP/RTCP streams
+ * from the result media description.
+ * Indeed, when RTP bundle mode is ON, this information is to be taken in the transport owner stream.
+ */
+void MS2Stream::getRtpDestination(const OfferAnswerContext &params, RtpAddressInfo *info){
+	const SalStreamDescription *stream = params.resultStreamDescription;
+	if (mRtpBundle && !mOwnsBundle){
+		if (!mBundleOwner){
+			lError() << "Bundle owner shall be set !";
+		}else{
+			stream = &params.resultMediaDescription->streams[mBundleOwner->getIndex()];
+		}
+	}
+	
+	info->rtpAddr = stream->rtp_addr[0] != '\0' ? stream->rtp_addr : params.resultMediaDescription->addr;
+	bool isMulticast = !!ms_is_multicast(info->rtpAddr.c_str());
+	info->rtpPort = stream->rtp_port;
+	info->rtcpAddr = stream->rtcp_addr[0] != '\0' ? stream->rtcp_addr : info->rtpAddr;
+	info->rtcpPort = (linphone_core_rtcp_enabled(getCCore()) && !isMulticast) ? (stream->rtcp_port ? stream->rtcp_port : stream->rtp_port + 1) : 0;
+}
+
+/*
+ * Handle some basic session changes.
+ * Return true everything was handled, false otherwise, in which case the caller will have to restart the stream.
+ */
+bool MS2Stream::handleBasicChanges(const OfferAnswerContext &params, CallSession::State targetState){
+	const SalStreamDescription *stream = params.resultStreamDescription;
+	
+	if (stream->dir == SalStreamInactive || !sal_stream_description_enabled(stream)){
+		/* In this case all we have to do is to ensure that the stream is stopped. */
+		if (getState() != Stopped) stop();
+		return true;
+	}
+	if (getState() == Stream::Running){
+		int changesToHandle = params.resultStreamDescriptionChanges;
+		if (params.resultStreamDescriptionChanges & SAL_MEDIA_DESCRIPTION_NETWORK_CHANGED){
+			updateDestinations(params);
+			changesToHandle &= ~SAL_MEDIA_DESCRIPTION_NETWORK_CHANGED;
+		}
+		if (params.resultStreamDescriptionChanges & SAL_MEDIA_DESCRIPTION_CRYPTO_KEYS_CHANGED){
+			updateCryptoParameters(params);
+			changesToHandle &= ~SAL_MEDIA_DESCRIPTION_CRYPTO_KEYS_CHANGED;
+		}
+		// SAL_MEDIA_DESCRIPTION_STREAMS_CHANGED monitors the number of streams, it is ignored here.
+		changesToHandle &= ~SAL_MEDIA_DESCRIPTION_STREAMS_CHANGED;
+		
+		if (changesToHandle == 0){
+			// We've handled everything.
+			if (params.resultStreamDescriptionChanges){
+				lInfo() << "Stream updated, no need to restart.";
+			}
+			return true;
+		}
+	}else if (getState() == Stream::Stopped){
+		/* Already stopped, nothing to do.*/
+		return false;
+	}
+	/* Otherwise these changes shall be handled by a full restart of the stream. */
+	stop();
+	return false;
+}
+
+void MS2Stream::render(const OfferAnswerContext &params, CallSession::State targetState){
+	const SalStreamDescription *stream = params.resultStreamDescription;
+	const char *rtpAddr = (stream->rtp_addr[0] != '\0') ? stream->rtp_addr : params.resultMediaDescription->addr;
+	bool isMulticast = !!ms_is_multicast(rtpAddr);
+	MediaStream *ms = getMediaStream();
+	
+	if (getIceService().isActive() || (getMediaSessionPrivate().getParams()->earlyMediaSendingEnabled() 
+		&& (targetState == CallSession::State::OutgoingEarlyMedia))) {
+		rtp_session_set_symmetric_rtp(mSessions.rtp_session, false);
+	}
+	media_stream_set_max_network_bitrate(getMediaStream(), linphone_core_get_upload_bandwidth(getCCore()) * 1000);
+	if (isMulticast)
+		rtp_session_set_multicast_ttl(mSessions.rtp_session, stream->ttl);
+	rtp_session_enable_rtcp_mux(mSessions.rtp_session, stream->rtcp_mux);
+		// Valid local tags are > 0
+	if (sal_stream_description_has_srtp(stream)) {
+		int cryptoIdx = Sal::findCryptoIndexFromTag(params.localStreamDescription->crypto, static_cast<unsigned char>(stream->crypto_local_tag));
+		if (cryptoIdx >= 0) {
+			ms_media_stream_sessions_set_srtp_recv_key_b64(&ms->sessions, stream->crypto[0].algo, stream->crypto[0].master_key);
+			ms_media_stream_sessions_set_srtp_send_key_b64(&ms->sessions, stream->crypto[0].algo, 
+								       params.localStreamDescription->crypto[cryptoIdx].master_key);
+		} else
+			lWarning() << "Failed to find local crypto algo with tag: " << stream->crypto_local_tag;
+	}
+	ms_media_stream_sessions_set_encryption_mandatory(&ms->sessions, getMediaSessionPrivate().isEncryptionMandatory());
+	configureRtpSessionForRtcpFb(params);
+	configureRtpSessionForRtcpXr(params);
+	configureAdaptiveRateControl(params);
+	
+	if (stream->dtls_role != SalDtlsRoleInvalid){ /* If DTLS is available at both end points */
+		/* Give the peer certificate fingerprint to dtls context */
+		ms_dtls_srtp_set_peer_fingerprint(ms->sessions.dtls_context, params.remoteStreamDescription->dtls_fingerprint);
+	}
+	
+	switch(targetState){
+		case CallSession::State::IncomingEarlyMedia:
+			BCTBX_NO_BREAK;
+		case CallSession::State::OutgoingEarlyMedia:
+			/* don't accept to send real-live media in early media stage by default.*/
+			if (!getMediaSessionPrivate().getParams()->earlyMediaSendingEnabled()) {
+				lInfo() << "Early media sending not allowed, will send silence and dummy video instead.";
+				mMuted = true;
+			}else{
+				lInfo() << "Early media sending allowed, will send real live sound and video.";
+			}
+		break;
+		case CallSession::State::StreamsRunning:
+			mMuted = false;
+			finishEarlyMediaForking();
+		break;
+		default:
+		break;
+	}
+	startEventHandling();
+	initRtpBundle(params);
+	setIceCheckList(mIceCheckList); // do it after enabling bundles
+	Stream::render(params, targetState);
+}
+
+
+OrtpJitterBufferAlgorithm MS2Stream::jitterBufferNameToAlgo (const string &name) {
+	if (name == "basic") return OrtpJitterBufferBasic;
+	if (name == "rls") return OrtpJitterBufferRecursiveLeastSquare;
+	lError() << "Invalid jitter buffer algorithm: " << name;
+	return OrtpJitterBufferRecursiveLeastSquare;
+}
+
+void MS2Stream::applyJitterBufferParams (RtpSession *session) {
+	LinphoneConfig *config = linphone_core_get_config(getCCore());
+	JBParameters params;
+	rtp_session_get_jitter_buffer_params(session, &params);
+	params.min_size = lp_config_get_int(config, "rtp", "jitter_buffer_min_size", 40);
+	params.max_size = lp_config_get_int(config, "rtp", "jitter_buffer_max_size", 500);
+	params.max_packets = params.max_size * 200 / 1000; /* Allow 200 packet per seconds, quite large */
+	const char *algo = lp_config_get_string(config, "rtp", "jitter_buffer_algorithm", "rls");
+	params.buffer_algorithm = jitterBufferNameToAlgo(algo ? algo : "");
+	params.refresh_ms = lp_config_get_int(config, "rtp", "jitter_buffer_refresh_period", 5000);
+	params.ramp_refresh_ms = lp_config_get_int(config, "rtp", "jitter_buffer_ramp_refresh_period", 5000);
+	params.ramp_step_ms = lp_config_get_int(config, "rtp", "jitter_buffer_ramp_step", 20);
+	params.ramp_threshold = lp_config_get_int(config, "rtp", "jitter_buffer_ramp_threshold", 70);
+
+	switch (getType()) {
+		case SalAudio:
+		case SalText: /* Let's use the same params for text as for audio */
+			params.nom_size = linphone_core_get_audio_jittcomp(getCCore());
+			params.adaptive = linphone_core_audio_adaptive_jittcomp_enabled(getCCore());
+			break;
+		case SalVideo:
+			params.nom_size = linphone_core_get_video_jittcomp(getCCore());
+			params.adaptive = linphone_core_video_adaptive_jittcomp_enabled(getCCore());
+			break;
+		default:
+			lError() << "applyJitterBufferParams(): should not happen";
+			break;
+	}
+	params.enabled = params.nom_size > 0;
+	if (params.enabled) {
+		if (params.min_size > params.nom_size)
+			params.min_size = params.nom_size;
+		if (params.max_size < params.nom_size)
+			params.max_size = params.nom_size;
+	}
+	rtp_session_set_jitter_buffer_params(session, &params);
+}
+
+void MS2Stream::configureRtpSession(RtpSession *session){
+	rtp_session_enable_network_simulation(session, &getCCore()->net_conf.netsim_params);
+	applyJitterBufferParams(session);
+	string userAgent = linphone_core_get_user_agent(getCCore());
+	rtp_session_set_source_description(session, getMediaSessionPrivate().getMe()->getAddress().asString().c_str(), NULL, NULL, NULL, NULL, userAgent.c_str(), NULL);
+	rtp_session_set_symmetric_rtp(session, linphone_core_symmetric_rtp_enabled(getCCore()));
+	
+	if (getType() == SalVideo){
+		int videoRecvBufSize = lp_config_get_int(linphone_core_get_config(getCCore()), "video", "recv_buf_size", 0);
+		if (videoRecvBufSize > 0)
+			rtp_session_set_recv_buf_size(session, videoRecvBufSize);
+	}
+}
+
+void MS2Stream::setupDtlsParams (MediaStream *ms) {
+	if (getMediaSessionPrivate().getParams()->getMediaEncryption() == LinphoneMediaEncryptionDTLS) {
+		MSDtlsSrtpParams dtlsParams = { 0 };
+		
+		/* TODO : search for a certificate with CNAME=sip uri(retrieved from variable me) or default : linphone-dtls-default-identity */
+		/* This will parse the directory to find a matching fingerprint or generate it if not found */
+		/* returned string must be freed */
+		char *certificate = nullptr;
+		char *key = nullptr;
+		char *fingerprint = nullptr;
+
+		sal_certificates_chain_parse_directory(&certificate, &key, &fingerprint,
+			linphone_core_get_user_certificates_path(getCCore()), "linphone-dtls-default-identity", SAL_CERTIFICATE_RAW_FORMAT_PEM, true, true);
+		if (fingerprint) {
+			if (getMediaSessionPrivate().getDtlsFingerprint().empty()){
+				getMediaSessionPrivate().setDtlsFingerprint(fingerprint);
+			}
+			mDtlsFingerPrint = fingerprint;
+			ms_free(fingerprint);
+		}
+		if (key && certificate) {
+			dtlsParams.pem_certificate = certificate;
+			dtlsParams.pem_pkey = key;
+			dtlsParams.role = MSDtlsSrtpRoleUnset; /* Default is unset, then check if we have a result SalMediaDescription */
+			media_stream_enable_dtls(ms, &dtlsParams);
+			ms_free(certificate);
+			ms_free(key);
+		} else {
+			lError() << "Unable to retrieve or generate DTLS certificate and key - DTLS disabled";
+			/* TODO : check if encryption forced, if yes, stop call */
+		}
+	}
+}
+
+void MS2Stream::startDtls(const OfferAnswerContext &params){
+	if (mDtlsStarted) return;
+	if (!sal_stream_description_has_dtls(params.resultStreamDescription)) return;
+	
+	if (params.resultStreamDescription->dtls_role == SalDtlsRoleInvalid){
+		lWarning() << "Unable to start DTLS engine on stream session [" << &mSessions << "], Dtls role in resulting media description is invalid";
+	}else {
+		if (!isTransportOwner()){
+			/* RTP bundle mode: there must be only one DTLS association per transport. */
+			return;
+		}
+		/* Workaround for buggy openssl versions that send DTLS packets bigger than the MTU. We need to increase the recv buf size of the RtpSession.*/
+		int recv_buf_size = lp_config_get_int(linphone_core_get_config(getCCore()),"rtp", "dtls_recv_buf_size", 5000);
+		rtp_session_set_recv_buf_size(mSessions.rtp_session, recv_buf_size);
+		
+		/* If DTLS is available at both end points */
+		/* Give the peer certificate fingerprint to dtls context */
+		ms_dtls_srtp_set_peer_fingerprint(mSessions.dtls_context, params.remoteStreamDescription->dtls_fingerprint);
+		ms_dtls_srtp_set_role(mSessions.dtls_context, (params.resultStreamDescription->dtls_role == SalDtlsRoleIsClient) ? MSDtlsSrtpRoleIsClient : MSDtlsSrtpRoleIsServer); /* Set the role to client */
+		ms_dtls_srtp_start(mSessions.dtls_context); /* Then start the engine, it will send the DTLS client Hello */
+		mDtlsStarted = true;
+	}
+}
+
+void MS2Stream::initializeSessions(MediaStream *stream){
+	if (mPortConfig.multicastRole == SalMulticastReceiver){
+		if (!mPortConfig.multicastIp.empty())
+			media_stream_join_multicast_group(stream, mPortConfig.multicastIp.c_str());
+		else
+			lError() << "Cannot join multicast group if multicast ip is not set";
+	}
+	
+	configureRtpSession(stream->sessions.rtp_session);
+	setupDtlsParams(stream);
+	
+	if (mPortConfig.rtpPort == -1){
+		// Case where we requested random ports from the system. Now that they are allocated, get them.
+		mPortConfig.rtpPort = rtp_session_get_local_port(stream->sessions.rtp_session);
+		mPortConfig.rtcpPort = rtp_session_get_local_rtcp_port(stream->sessions.rtp_session);
+	}
+	int dscp = -1;
+	switch(getType()){
+		case SalAudio:
+			dscp = linphone_core_get_audio_dscp(getCCore());
+		break;
+		case SalVideo:
+			dscp = linphone_core_get_video_dscp(getCCore());
+		break;
+		default:
+		break;
+		
+	}
+	if (dscp != -1)
+		media_stream_set_dscp(stream, dscp);
+	
+	mOrtpEvQueue = ortp_ev_queue_new();
+	rtp_session_register_event_queue(stream->sessions.rtp_session, mOrtpEvQueue);
+	
+	media_stream_reclaim_sessions(stream, &mSessions);
+	
+}
+
+void MS2Stream::updateCryptoParameters(const OfferAnswerContext &params) {
+	const SalStreamDescription *localStreamDesc = params.localStreamDescription;
+	const SalStreamDescription *newStream = params.resultStreamDescription;
+	MediaStream * ms = getMediaStream();
+	
+	if (newStream->proto == SalProtoRtpSavpf || newStream->proto == SalProtoRtpSavp){
+		int cryptoIdx = Sal::findCryptoIndexFromTag(localStreamDesc->crypto, static_cast<unsigned char>(newStream->crypto_local_tag));
+		if (cryptoIdx >= 0) {
+			if (params.localStreamDescriptionChanges & SAL_MEDIA_DESCRIPTION_CRYPTO_KEYS_CHANGED){
+				ms_media_stream_sessions_set_srtp_send_key_b64(&ms->sessions, newStream->crypto[0].algo, localStreamDesc->crypto[cryptoIdx].master_key);
+			}
+			ms_media_stream_sessions_set_srtp_recv_key_b64(&ms->sessions, newStream->crypto[0].algo, newStream->crypto[0].master_key);
+		} else
+			lWarning() << "Failed to find local crypto algo with tag: " << newStream->crypto_local_tag;
+	}
+	startDtls(params);
+}
+
+void MS2Stream::updateDestinations(const OfferAnswerContext &params) {
+	if (params.resultStreamDescription->rtp_port == 0 && params.resultStreamDescription->bundle_only){
+		/* we can ignore */
+		return;
+	}
+	
+	const char *rtpAddr = (params.resultStreamDescription->rtp_addr[0] != '\0') ? params.resultStreamDescription->rtp_addr : params.resultMediaDescription->addr;
+	const char *rtcpAddr = (params.resultStreamDescription->rtcp_addr[0] != '\0') ? params.resultStreamDescription->rtcp_addr : params.resultMediaDescription->addr;
+	lInfo() << "Change audio stream destination: RTP=" << rtpAddr << ":" << params.resultStreamDescription->rtp_port << " RTCP=" << rtcpAddr << ":" << params.resultStreamDescription->rtcp_port;
+	rtp_session_set_remote_addr_full(mSessions.rtp_session, rtpAddr, params.resultStreamDescription->rtp_port, rtcpAddr, params.resultStreamDescription->rtcp_port);
+}
+
+void MS2Stream::startEventHandling(){
+	if (mTimer) return;
+	mTimer = getCore().createTimer([this](){
+			handleEvents();
+			return true;
+		}, sEventPollIntervalMs, "Stream event processing timer");
+}
+
+void MS2Stream::stopEventHandling(){
+	if (mTimer){
+		getCore().destroyTimer(mTimer);
+		mTimer = nullptr;
+	}
+}
+
+bool MS2Stream::prepare(){
+	if (getCCore()->rtptf) {
+		RtpTransport *meta_rtp;
+		RtpTransport *meta_rtcp;
+		rtp_session_get_transports(mSessions.rtp_session, &meta_rtp, &meta_rtcp);
+		LinphoneCoreRtpTransportFactoryFunc rtpFunc = nullptr, rtcpFunc = nullptr;
+		void *rtpFuncData = nullptr, *rtcpFuncData = nullptr;
+		
+		switch(getType()){
+			case SalAudio:
+				rtpFunc = getCCore()->rtptf->audio_rtp_func;
+				rtpFuncData = getCCore()->rtptf->audio_rtp_func_data;
+				rtcpFunc = getCCore()->rtptf->audio_rtcp_func;
+				rtcpFuncData = getCCore()->rtptf->audio_rtcp_func_data;
+			break;
+			case SalVideo:
+				rtpFunc = getCCore()->rtptf->video_rtp_func;
+				rtpFuncData = getCCore()->rtptf->video_rtp_func_data;
+				rtcpFunc = getCCore()->rtptf->video_rtcp_func;
+				rtcpFuncData = getCCore()->rtptf->video_rtcp_func_data;
+			break;
+			case SalText:
+			break;
+			case SalOther:
+			break;
+		}
+		
+		if (!meta_rtp_transport_get_endpoint(meta_rtp)) {
+			lInfo() << this << " using custom RTP transport endpoint";
+			meta_rtp_transport_set_endpoint(meta_rtp, rtpFunc(rtpFuncData, mPortConfig.rtpPort));
+		}
+		if (!meta_rtp_transport_get_endpoint(meta_rtcp))
+			meta_rtp_transport_set_endpoint(meta_rtcp, rtcpFunc(rtcpFuncData, mPortConfig.rtcpPort));
+	}
+	setIceCheckList(mIceCheckList);
+	startEventHandling();
+	Stream::prepare();
+	return false;
+}
+
+void MS2Stream::finishPrepare(){
+	Stream::finishPrepare();
+	stopEventHandling();
+}
+
+int MS2Stream::getIdealAudioBandwidth (const SalMediaDescription *md, const SalStreamDescription *desc) {
+	int remoteBandwidth = 0;
+	if (desc->bandwidth > 0)
+		remoteBandwidth = desc->bandwidth;
+	else if (md->bandwidth > 0) {
+		/* Case where b=AS is given globally, not per stream */
+		remoteBandwidth = md->bandwidth;
+	}
+	int uploadBandwidth = 0;
+	bool forced = false;
+	if (getMediaSessionPrivate().getParams()->getPrivate()->getUpBandwidth() > 0) {
+		forced = true;
+		uploadBandwidth = getMediaSessionPrivate().getParams()->getPrivate()->getUpBandwidth();
+	} else
+		uploadBandwidth = linphone_core_get_upload_bandwidth(getCCore());
+	uploadBandwidth = PayloadTypeHandler::getMinBandwidth(uploadBandwidth, remoteBandwidth);
+	if (!linphone_core_media_description_contains_video_stream(md) || forced)
+		return uploadBandwidth;
+	
+	/*
+	 * This a default heuristic to choose a target upload bandwidth for an audio stream, the
+	 * remaining can then be allocated for video.
+	 */
+	if (PayloadTypeHandler::bandwidthIsGreater(uploadBandwidth, 512))
+		uploadBandwidth = 100;
+	else if (PayloadTypeHandler::bandwidthIsGreater(uploadBandwidth, 256))
+		uploadBandwidth = 64;
+	else if (PayloadTypeHandler::bandwidthIsGreater(uploadBandwidth, 128))
+		uploadBandwidth = 40;
+	else if (PayloadTypeHandler::bandwidthIsGreater(uploadBandwidth, 0))
+		uploadBandwidth = 24;
+	return uploadBandwidth;
+}
+
+RtpProfile * MS2Stream::makeProfile(const SalMediaDescription *md, const SalStreamDescription *desc, int *usedPt) {
+	if (mRtpProfile){
+		rtp_profile_destroy(mRtpProfile);
+		mRtpProfile = nullptr;
+	}
+	*usedPt = -1;
+	int bandwidth = 0;
+	if (desc->type == SalAudio)
+		bandwidth = getIdealAudioBandwidth(md, desc);
+	else if (desc->type == SalVideo)
+		bandwidth = getGroup().getVideoBandwidth(md, desc);
+
+	bool first = true;
+	RtpProfile *profile = rtp_profile_new("Call profile");
+	for (const bctbx_list_t *elem = desc->payloads; elem != nullptr; elem = bctbx_list_next(elem)) {
+		OrtpPayloadType *pt = reinterpret_cast<OrtpPayloadType *>(bctbx_list_get_data(elem));
+		/* Make a copy of the payload type, so that we left the ones from the SalStreamDescription unchanged.
+		 * If the SalStreamDescription is freed, this will have no impact on the running streams. */
+		pt = payload_type_clone(pt);
+		int upPtime = 0;
+		if ((pt->flags & PAYLOAD_TYPE_FLAG_CAN_SEND) && first) {
+			/* First codec in list is the selected one */
+			if (desc->type == SalAudio) {
+				bandwidth = getGroup().updateAllocatedAudioBandwidth(pt, bandwidth);
+				upPtime = getMediaSessionPrivate().getParams()->getPrivate()->getUpPtime();
+				if (!upPtime)
+					upPtime = linphone_core_get_upload_ptime(getCCore());
+			}
+			first = false;
+		}
+		if (*usedPt == -1) {
+			/* Don't select telephone-event as a payload type */
+			if (strcasecmp(pt->mime_type, "telephone-event") != 0)
+				*usedPt = payload_type_get_number(pt);
+		}
+		if (pt->flags & PAYLOAD_TYPE_BITRATE_OVERRIDE) {
+			lInfo() << "Payload type [" << pt->mime_type << "/" << pt->clock_rate << "] has explicit bitrate [" << (pt->normal_bitrate / 1000) << "] kbit/s";
+			pt->normal_bitrate = PayloadTypeHandler::getMinBandwidth(pt->normal_bitrate, bandwidth * 1000);
+		} else
+			pt->normal_bitrate = bandwidth * 1000;
+		if (desc->maxptime > 0) {// follow the same schema for maxptime as for ptime. (I.E add it to fmtp)
+			ostringstream os;
+			os << "maxptime=" << desc->maxptime;
+			payload_type_append_send_fmtp(pt, os.str().c_str());
+		}
+		if (desc->ptime > 0)
+			upPtime = desc->ptime;
+		if (upPtime > 0) {
+			ostringstream os;
+			os << "ptime=" << upPtime;
+			payload_type_append_send_fmtp(pt, os.str().c_str());
+		}
+		int number = payload_type_get_number(pt);
+		if (rtp_profile_get_payload(profile, number))
+			lWarning() << "A payload type with number " << number << " already exists in profile!";
+		else
+			rtp_profile_set_payload(profile, number, pt);
+	}
+	mRtpProfile = profile;
+	return profile;
+}
+
+
+void MS2Stream::updateStats(){
+	if (mSessions.rtp_session) {
+		const rtp_stats_t *rtpStats = rtp_session_get_stats(mSessions.rtp_session);
+		if (rtpStats)
+			_linphone_call_stats_set_rtp_stats(mStats, rtpStats);
+	}
+	float quality = media_stream_get_average_quality_rating(getMediaStream());
+	LinphoneCallLog *log = getMediaSession().getLog();
+	if (quality >= 0) {
+		if (log->quality == -1.0)
+			log->quality = quality;
+		else
+			log->quality *= quality / 5.0f;
+	}
+}
+
+LinphoneCallStats *MS2Stream::getStats(){
+	MediaStream *ms = getMediaStream();
+	if (ms) linphone_call_stats_update(mStats, ms);
+	return mStats;
+}
+
+void MS2Stream::stop(){
+	CallSessionListener *listener = getMediaSessionPrivate().getCallSessionListener();
+	
+	if (listener){
+		int statsType = -1;
+		switch(getType()){
+			case SalAudio: statsType = LINPHONE_CALL_STATS_AUDIO; break;
+			case SalVideo: statsType = LINPHONE_CALL_STATS_VIDEO; break;
+			case SalText: statsType = LINPHONE_CALL_STATS_TEXT; break;
+			default:
+				break;
+			
+		}
+		
+		if (statsType != -1) listener->onUpdateMediaInfoForReporting(getMediaSession().getSharedFromThis(), statsType);
+		
+		/*
+		 * FIXME : very very ugly way to manage the conference. Worse, it can remove from a conference a stream that has never been part 
+		 * of any conference.
+		 * Solution: let the Conference object manage the StreamsGroups that are part of a conference.
+		 */
+		if (getType() == SalAudio) listener->onCallSessionConferenceStreamStopping(getMediaSession().getSharedFromThis());
+	}
+	ms_bandwidth_controller_remove_stream(getCCore()->bw_controller, getMediaStream());
+	updateStats();
+	handleEvents();
+	stopEventHandling();
+	media_stream_reclaim_sessions(getMediaStream(), &mSessions);
+	rtp_session_set_profile(mSessions.rtp_session, &av_profile);
+	Stream::stop();
+	
+	/* At this time the derived class hasn't yet stopped it streams.
+	 * the RTP Profile objects can't be destroyed until the stream is completely stopped.
+	 * As a result we do it later*/
+	RtpProfile *rtpProfile = mRtpProfile;
+	RtpProfile *rtpIoProfile = mRtpIoProfile;
+	getCore().doLater( [rtpProfile, rtpIoProfile](){
+		if (rtpProfile) rtp_profile_destroy(rtpProfile);
+		if (rtpIoProfile) rtp_profile_destroy(rtpIoProfile);
+	});
+	mRtpProfile = nullptr;
+	mRtpIoProfile = nullptr;
+}
+
+void MS2Stream::notifyStatsUpdated () {
+	CallSessionListener *listener = getMediaSessionPrivate().getCallSessionListener();
+	if (_linphone_call_stats_get_updated(mStats)) {
+		switch (_linphone_call_stats_get_updated(mStats)) {
+			case LINPHONE_CALL_STATS_RECEIVED_RTCP_UPDATE:
+			case LINPHONE_CALL_STATS_SENT_RTCP_UPDATE:
+				if (listener) {
+					listener->onRtcpUpdateForReporting(getMediaSession().getSharedFromThis(), getType());
+				}
+				break;
+			default:
+				break;
+		}
+		if (listener)
+			listener->onStatsUpdated(getMediaSession().getSharedFromThis(), mStats);
+		_linphone_call_stats_set_updated(mStats, 0);
+	}
+}
+
+void MS2Stream::iceStateChanged(){
+	updateIceInStats();
+}
+
+void MS2Stream::updateIceInStats(LinphoneIceState state){
+	lInfo() << "ICE state is " << linphone_ice_state_to_string(state) << " for " << *this;
+	_linphone_call_stats_set_ice_state(mStats, state);
+}
+
+void MS2Stream::updateIceInStats(){
+	/* Special case for rtp bundle: we report the ice state of the transport owner. */
+	if (mRtpBundle && !mOwnsBundle && mBundleOwner && mBundleOwner->mStats){
+		updateIceInStats(linphone_call_stats_get_ice_state(mBundleOwner->mStats));
+		return;
+	}
+	
+	if (!mIceCheckList){
+		updateIceInStats(LinphoneIceStateNotActivated);
+		return;
+	}
+	if (ice_check_list_state(mIceCheckList) == ICL_Failed) {
+		updateIceInStats(LinphoneIceStateFailed);
+		return;
+	}
+	if (ice_check_list_state(mIceCheckList) == ICL_Running) {
+		updateIceInStats(LinphoneIceStateInProgress);
+		return;
+	}
+	/* Otherwise we are in ICL_Completed state. */
+
+	switch (ice_check_list_selected_valid_candidate_type(mIceCheckList)) {
+		case ICT_HostCandidate:
+			updateIceInStats(LinphoneIceStateHostConnection);
+			break;
+		case ICT_ServerReflexiveCandidate:
+		case ICT_PeerReflexiveCandidate:
+			updateIceInStats(LinphoneIceStateReflexiveConnection);
+			break;
+		case ICT_RelayedCandidate:
+			updateIceInStats(LinphoneIceStateRelayConnection);
+			break;
+		case ICT_CandidateInvalid:
+		case ICT_CandidateTypeMax:
+			// Shall not happen.
+			L_ASSERT(false);
+			break;
+	}
+}
+
+void MS2Stream::dtlsEncryptionChanged(){
+	getGroup().propagateEncryptionChanged();
+}
+
+void MS2Stream::handleEvents () {
+	MediaStream *ms = getMediaStream();
+	if (ms) {
+		switch(ms->type){
+			case MSAudio:
+				audio_stream_iterate((AudioStream *)ms);
+				break;
+			case MSVideo:
+#ifdef VIDEO_ENABLED
+				video_stream_iterate((VideoStream *)ms);
+#endif
+				break;
+			case MSText:
+				text_stream_iterate((TextStream *)ms);
+				break;
+			default:
+				lError() << "handleStreamEvents(): unsupported stream type";
+				return;
+		}
+	}
+	OrtpEvent *ev;
+	
+	while ((ev = ortp_ev_queue_get(mOrtpEvQueue)) != nullptr) {
+		OrtpEventType evt = ortp_event_get_type(ev);
+		OrtpEventData *evd = ortp_event_get_data(ev);
+
+		/*This MUST be done before any call to "linphone_call_stats_fill" since it has ownership over evd->packet*/
+		if (evt == ORTP_EVENT_RTCP_PACKET_RECEIVED) {
+			do {
+				if (evd->packet && rtcp_is_RTPFB(evd->packet)) {
+					if (rtcp_RTPFB_get_type(evd->packet) == RTCP_RTPFB_TMMBR) {
+						CallSessionListener *listener = getMediaSessionPrivate().getCallSessionListener();
+						listener->onTmmbrReceived(getMediaSession().getSharedFromThis(), (int)getIndex(), (int)rtcp_RTPFB_tmmbr_get_max_bitrate(evd->packet));
+					}
+				}
+			} while (rtcp_next_packet(evd->packet));
+			rtcp_rewind(evd->packet);
+		}
+
+		if (ms)
+			linphone_call_stats_fill(mStats, ms, ev);
+		switch(evt){
+			case ORTP_EVENT_ZRTP_ENCRYPTION_CHANGED:
+				if (getType() != SalAudio || !isMain()){
+					getGroup().propagateEncryptionChanged();
+				}
+			break;
+			case ORTP_EVENT_DTLS_ENCRYPTION_CHANGED:
+				dtlsEncryptionChanged();
+			break;
+			case ORTP_EVENT_ICE_SESSION_PROCESSING_FINISHED:
+			case ORTP_EVENT_ICE_GATHERING_FINISHED:
+			case ORTP_EVENT_ICE_LOSING_PAIRS_COMPLETED:
+			case ORTP_EVENT_ICE_RESTART_NEEDED:
+				/* ICE events are notified directly to the IceService. */
+				getIceService().handleIceEvent(ev);
+			break;
+		}
+		notifyStatsUpdated();
+	
+		/* Let subclass handle the event.*/
+		handleEvent(ev);
+		ortp_event_destroy(ev);
+	}
+}
+
+bool MS2Stream::isEncrypted() const{
+	if (!isTransportOwner()){
+		if (mBundleOwner){
+			return mBundleOwner->isEncrypted(); /* We must refer to the stream that owns the Rtp bundle.*/
+		}else{
+			lError() << "MS2Stream::isEncrypted(): no bundle owner !";
+		}
+		return false;
+	}
+	return media_stream_secured(getMediaStream());
+}
+
+bool MS2Stream::isMuted()const{
+	return mMuted;
+}
+
+RtpSession* MS2Stream::createRtpIoSession() {
+	LinphoneConfig *config = linphone_core_get_config(getCCore());
+	const char *config_section = getType() == SalAudio ? "sound" : "video";
+	const char *rtpmap = lp_config_get_string(config, config_section, "rtp_map", getType() == SalAudio ? "pcmu/8000/1" : "vp8/90000");
+	OrtpPayloadType *pt = rtp_profile_get_payload_from_rtpmap(mRtpProfile, rtpmap);
+	if (!pt)
+		return nullptr;
+	string profileName = string("RTP IO ") + string(config_section) + string(" profile");
+	mRtpIoProfile = rtp_profile_new(profileName.c_str());
+	int ptnum = lp_config_get_int(config, config_section, "rtp_ptnum", 0);
+	rtp_profile_set_payload(mRtpIoProfile, ptnum, payload_type_clone(pt));
+	const char *localIp = lp_config_get_string(config, config_section, "rtp_local_addr", "127.0.0.1");
+	int localPort = lp_config_get_int(config, config_section, "rtp_local_port", 17076);
+	RtpSession *rtpSession = ms_create_duplex_rtp_session(localIp, localPort, -1, ms_factory_get_mtu(getCCore()->factory));
+	rtp_session_set_profile(rtpSession, mRtpIoProfile);
+	const char *remoteIp = lp_config_get_string(config, config_section, "rtp_remote_addr", "127.0.0.1");
+	int remotePort = lp_config_get_int(config, config_section, "rtp_remote_port", 17078);
+	rtp_session_set_remote_addr_and_port(rtpSession, remoteIp, remotePort, -1);
+	rtp_session_enable_rtcp(rtpSession, false);
+	rtp_session_set_payload_type(rtpSession, ptnum);
+	int jittcomp = lp_config_get_int(config, config_section, "rtp_jittcomp", 0); /* 0 means no jitter buffer */
+	rtp_session_set_jitter_compensation(rtpSession, jittcomp);
+	rtp_session_enable_jitter_buffer(rtpSession, (jittcomp > 0));
+	bool symmetric = !!lp_config_get_int(config, config_section, "rtp_symmetric", 0);
+	rtp_session_set_symmetric_rtp(rtpSession, symmetric);
+	return rtpSession;
+}
+
+std::pair<RtpTransport*, RtpTransport*> MS2Stream::getMetaRtpTransports(){
+	RtpTransport *metaRtp = nullptr;
+	RtpTransport *metaRtcp = nullptr;
+	rtp_session_get_transports(mSessions.rtp_session, &metaRtp, &metaRtcp);
+	return make_pair(metaRtp, metaRtcp);
+}
+
+MSZrtpContext *MS2Stream::getZrtpContext()const{
+	return mSessions.zrtp_context;
+}
+
+float MS2Stream::getAverageQuality(){
+	MediaStream *ms = getMediaStream();
+	if (!ms) {
+		lError() << "MS2Stream::getAverageQuality(): no stream.";
+		return 0.0;
+	}
+	return media_stream_get_average_quality_rating(ms);
+}
+
+float MS2Stream::getCurrentQuality(){
+	MediaStream *ms = getMediaStream();
+	if (!ms) {
+		lError() << "MS2Stream::getCurrentQuality(): no stream.";
+		return 0.0;
+	}
+	return media_stream_get_quality_rating(getMediaStream());
+}
+
+void MS2Stream::updateBandwidthReports(){
+	MediaStream * ms = getMediaStream();
+	bool active = ms ? (media_stream_get_state(ms) == MSStreamStarted) : false;
+	_linphone_call_stats_set_download_bandwidth(mStats, active ? (float)(media_stream_get_down_bw(ms) * 1e-3) : 0.f);
+	_linphone_call_stats_set_upload_bandwidth(mStats, active ? (float)(media_stream_get_up_bw(ms) * 1e-3) : 0.f);
+	_linphone_call_stats_set_rtcp_download_bandwidth(mStats, active ? (float)(media_stream_get_rtcp_down_bw(ms) * 1e-3) : 0.f);
+	_linphone_call_stats_set_rtcp_upload_bandwidth(mStats, active ? (float)(media_stream_get_rtcp_up_bw(ms) * 1e-3) : 0.f);
+	_linphone_call_stats_set_ip_family_of_remote(mStats,
+		active ? (ortp_stream_is_ipv6(&mSessions.rtp_session->rtp.gs) ? LinphoneAddressFamilyInet6 : LinphoneAddressFamilyInet) : LinphoneAddressFamilyUnspec);
+
+	if (getCCore()->send_call_stats_periodical_updates) {
+		CallSessionListener *listener = getMediaSessionPrivate().getCallSessionListener();
+		if (active)
+			linphone_call_stats_update(mStats, ms);
+		_linphone_call_stats_set_updated(mStats, _linphone_call_stats_get_updated(mStats) | LINPHONE_CALL_STATS_PERIODICAL_UPDATE);
+		if (listener)
+			listener->onStatsUpdated(getMediaSession().getSharedFromThis(), mStats);
+		_linphone_call_stats_set_updated(mStats, 0);
+	}
+}
+
+float MS2Stream::getCpuUsage()const{
+	MediaStream *ms = getMediaStream();
+	if (ms->sessions.ticker == nullptr) return 0.0f;
+	return ms_ticker_get_average_load(ms->sessions.ticker);
+}
+
+void MS2Stream::finish(){
+	if (mRtpBundle && mOwnsBundle){
+		rtp_bundle_delete(mRtpBundle);
+		mRtpBundle = nullptr;
+	}
+	if (mOrtpEvQueue){
+		rtp_session_unregister_event_queue(mSessions.rtp_session, mOrtpEvQueue);
+		ortp_ev_queue_flush(mOrtpEvQueue);
+		ortp_ev_queue_destroy(mOrtpEvQueue);
+		mOrtpEvQueue = nullptr;
+	}
+	ms_media_stream_sessions_uninit(&mSessions);
+	Stream::finish();
+}
+
+bool MS2Stream::avpfEnabled() const{
+	return media_stream_avpf_enabled(getMediaStream());
+}
+
+bool MS2Stream::bundleEnabled() const{
+	return mRtpBundle != nullptr;
+}
+
+bool MS2Stream::isTransportOwner() const{
+	bool ret = mRtpBundle == nullptr || mOwnsBundle;
+	return ret;
+}
+
+int MS2Stream::getAvpfRrInterval()const{
+	MediaStream *ms = getMediaStream();
+	return media_stream_get_state(ms) == MSStreamStarted ? media_stream_get_avpf_rr_interval(ms) : 0;
+}
+
+MS2Stream::~MS2Stream(){
+	finish();
+	linphone_call_stats_unref(mStats);
+	mStats = nullptr;
+	
+}
+
+
+
+LINPHONE_END_NAMESPACE
+
diff --git a/src/conference/session/ms2-streams.h b/src/conference/session/ms2-streams.h
new file mode 100644
index 0000000000000000000000000000000000000000..8f9e49ea188966e0e476433c72588219107c642e
--- /dev/null
+++ b/src/conference/session/ms2-streams.h
@@ -0,0 +1,258 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ms2_streams_h
+#define ms2_streams_h
+
+#include "streams.h"
+
+LINPHONE_BEGIN_NAMESPACE
+
+/**
+ * Derived class for streams commonly handly through mediastreamer2 library.
+ */
+class MS2Stream : public Stream, public RtpInterface {
+public:
+	virtual void fillLocalMediaDescription(OfferAnswerContext & ctx) override;
+	virtual bool prepare() override;
+	virtual void finishPrepare() override;
+	virtual void render(const OfferAnswerContext & ctx, CallSession::State targetState) override;
+	virtual void stop() override;
+	virtual void finish() override;
+	virtual bool isEncrypted() const override;
+	MSZrtpContext *getZrtpContext()const;
+	std::pair<RtpTransport*, RtpTransport*> getMetaRtpTransports();
+	virtual MediaStream *getMediaStream()const = 0;
+	virtual void tryEarlyMediaForking(const OfferAnswerContext &ctx) override;
+	virtual void finishEarlyMediaForking() override;
+	virtual float getCurrentQuality() override;
+	virtual float getAverageQuality() override;
+	virtual LinphoneCallStats *getStats() override;
+	virtual void startDtls(const OfferAnswerContext &params) override;
+	virtual bool isMuted()const override;
+	virtual void refreshSockets() override;
+	virtual void updateBandwidthReports() override;
+	virtual float getCpuUsage()const override;
+	virtual void setIceCheckList(IceCheckList *cl) override;
+	virtual void iceStateChanged() override;
+	
+	/* RtpInterface */
+	virtual bool avpfEnabled() const override;
+	virtual bool bundleEnabled() const override;
+	virtual int getAvpfRrInterval() const override;
+	virtual bool isTransportOwner() const override;
+	
+	virtual ~MS2Stream();
+protected:
+	virtual void handleEvent(const OrtpEvent *ev) = 0;
+	MS2Stream(StreamsGroup &sm, const OfferAnswerContext &params);
+	void startEventHandling();
+	void stopEventHandling();
+	std::string getBindIp();
+	int getBindPort();
+	void initializeSessions(MediaStream *stream);
+	RtpProfile * makeProfile(const SalMediaDescription *md, const SalStreamDescription *desc, int *usedPt);
+	int getIdealAudioBandwidth (const SalMediaDescription *md, const SalStreamDescription *desc);
+	RtpSession* createRtpIoSession();
+	void updateCryptoParameters(const OfferAnswerContext &params);
+	void updateDestinations(const OfferAnswerContext &params);
+	bool handleBasicChanges(const OfferAnswerContext &params, CallSession::State targetState);
+	struct RtpAddressInfo{
+		std::string rtpAddr;
+		std::string rtcpAddr;
+		int rtpPort, rtcpPort;
+	};
+	void getRtpDestination(const OfferAnswerContext &params, RtpAddressInfo *info);
+	void dtlsEncryptionChanged();
+	std::string mDtlsFingerPrint;
+	RtpProfile *mRtpProfile = nullptr;
+	RtpProfile *mRtpIoProfile = nullptr;
+	MSMediaStreamSessions mSessions;
+	OrtpEvQueue *mOrtpEvQueue = nullptr;
+	LinphoneCallStats *mStats = nullptr;
+	bool mUseAuxDestinations = false;
+	bool mMuted = false; /* to handle special cases where we want the audio to be muted - not related with linphone_core_enable_mic().*/
+	bool mDtlsStarted = false;
+private:
+	void initRtpBundle(const OfferAnswerContext &params);
+	RtpBundle *createOrGetRtpBundle(const SalStreamDescription *sd);
+	void removeFromBundle();
+	void notifyStatsUpdated();
+	void handleEvents();
+	void updateStats();
+	void initMulticast(const OfferAnswerContext &params);
+	void configureRtpSession(RtpSession *session);
+	void applyJitterBufferParams (RtpSession *session);
+	void setupDtlsParams(MediaStream *ms);
+	void configureRtpSessionForRtcpFb (const OfferAnswerContext &params);
+	void configureRtpSessionForRtcpXr(const OfferAnswerContext &params);
+	void configureAdaptiveRateControl(const OfferAnswerContext &params);
+	void updateIceInStats(LinphoneIceState state);
+	void updateIceInStats();
+	belle_sip_source_t *mTimer = nullptr;
+	IceCheckList *mIceCheckList = nullptr;
+	RtpBundle *mRtpBundle = nullptr;
+	MS2Stream *mBundleOwner = nullptr;
+	bool mOwnsBundle = false;
+	static OrtpJitterBufferAlgorithm jitterBufferNameToAlgo(const std::string &name);
+	static constexpr const int sEventPollIntervalMs = 20;
+};
+
+class MS2AudioStream : public MS2Stream, public AudioControlInterface{
+	friend class MS2VideoStream;
+public:
+	MS2AudioStream(StreamsGroup &sg, const OfferAnswerContext &params);
+	virtual bool prepare() override;
+	virtual void finishPrepare() override;
+	virtual void render(const OfferAnswerContext &ctx, CallSession::State targetState) override;
+	virtual void sessionConfirmed(const OfferAnswerContext &ctx) override;
+	virtual void stop() override;
+	virtual void finish() override;
+	
+	/* AudioControlInterface */
+	virtual void enableMic(bool value) override;
+	virtual void enableSpeaker(bool value) override;
+	virtual bool micEnabled()const override;
+	virtual bool speakerEnabled()const override;
+	virtual void startRecording() override;
+	virtual void stopRecording() override;
+	virtual bool isRecording() override{
+		return mRecordActive;
+	}
+	virtual float getPlayVolume() override;
+	virtual float getRecordVolume() override;
+	virtual float getMicGain() override;
+	virtual void setMicGain(float value) override;
+	virtual float getSpeakerGain() override;
+	virtual void setSpeakerGain(float value) override;
+	virtual void setRoute(LinphoneAudioRoute route) override;
+	virtual void sendDtmf(int dtmf) override;
+	virtual void enableEchoCancellation(bool value) override;
+	virtual bool echoCancellationEnabled()const override;
+	
+	virtual MediaStream *getMediaStream()const override;
+	virtual ~MS2AudioStream();
+	
+	/* Yeah quite ugly: this function is used externally to configure raw mediastreamer2 AudioStreams.*/
+	static void postConfigureAudioStream(AudioStream *as, LinphoneCore *lc, bool muted);
+	MSSndCard *getCurrentPlaybackCard()const{ return mCurrentPlaybackCard; }
+	MSSndCard *getCurrentCaptureCard()const{ return mCurrentCaptureCard; }
+	
+protected:
+	VideoStream *getPeerVideoStream();
+private:
+	virtual void handleEvent(const OrtpEvent *ev) override;
+	void setupMediaLossCheck();
+	void setPlaybackGainDb (float gain);
+	void setZrtpCryptoTypesParameters(MSZrtpParams *params, bool haveZrtpHash);
+	void startZrtpPrimaryChannel(const OfferAnswerContext &params);
+	static void parameterizeEqualizer(AudioStream *as, LinphoneCore *lc);
+	void forceSpeakerMuted(bool muted);
+	void postConfigureAudioStream(bool muted);
+	void setupRingbackPlayer();
+	void telephoneEventReceived (int event);
+	void configureAudioStream();
+	AudioStream *mStream = nullptr;
+	MSSndCard *mCurrentCaptureCard = nullptr;
+	MSSndCard *mCurrentPlaybackCard = nullptr;
+	belle_sip_source_t *mMediaLostCheckTimer = nullptr;
+	bool mMicMuted = false;
+	bool mSpeakerMuted = false;
+	bool mRecordActive = false;
+	bool mStartZrtpLater = false;
+	static constexpr const int ecStateMaxLen = 1048576; /* 1Mo */
+	static constexpr const char * ecStateStore = ".linphone.ecstate";
+};
+
+class MS2VideoStream : public MS2Stream, public VideoControlInterface{
+public:
+	MS2VideoStream(StreamsGroup &sg, const OfferAnswerContext &param);
+	virtual bool prepare() override;
+	virtual void finishPrepare() override;
+	virtual void render(const OfferAnswerContext &ctx, CallSession::State targetState) override;
+	virtual void stop() override;
+	virtual void finish() override;
+	
+	/* VideoControlInterface methods */
+	virtual void sendVfu() override;
+	virtual void sendVfuRequest() override;
+	virtual void enableCamera(bool value) override;
+	virtual bool cameraEnabled() const override;
+	virtual void setNativeWindowId(void *w) override;
+	virtual void * getNativeWindowId() const override;
+	virtual void setNativePreviewWindowId(void *w) override;
+	virtual void * getNativePreviewWindowId() const override;
+	virtual void tryEarlyMediaForking(const OfferAnswerContext &ctx) override;
+	virtual void parametersChanged() override;
+	virtual void requestNotifyNextVideoFrameDecoded () override;
+	virtual int takePreviewSnapshot (const std::string& file) override;
+	virtual int takeVideoSnapshot (const std::string& file) override;
+	virtual void zoomVideo (float zoomFactor, float cx, float cy) override;
+	virtual void getRecvStats(VideoStats *s) const override;
+	virtual void getSendStats(VideoStats *s) const override;
+	
+	virtual MediaStream *getMediaStream()const override;
+	
+	void oglRender();
+	MSWebCam * getVideoDevice(CallSession::State targetState)const;
+	
+	virtual ~MS2VideoStream();
+protected:
+	AudioStream *getPeerAudioStream();
+	
+private:
+	virtual void handleEvent(const OrtpEvent *ev) override;
+	virtual void zrtpStarted(Stream *mainZrtpStream) override;
+	static void sSnapshotTakenCb(void *userdata, struct _MSFilter *f, unsigned int id, void *arg);
+	void snapshotTakenCb(void *userdata, struct _MSFilter *f, unsigned int id, void *arg);
+	void videoStreamEventCb(const MSFilter *f, const unsigned int eventId, const void *args);
+	static void sVideoStreamEventCb (void *userData, const MSFilter *f, const unsigned int eventId, const void *args);
+	void activateZrtp();
+	VideoStream *mStream = nullptr;
+	void *mNativeWindowId = nullptr;
+	void *mNativePreviewWindowId = nullptr;
+	bool mCameraEnabled = true;
+	
+};
+
+/*
+ * Real time text stream.
+ */
+class MS2RTTStream : public MS2Stream{
+public:
+	MS2RTTStream(StreamsGroup &sm, const OfferAnswerContext &param);
+	virtual bool prepare() override;
+	virtual void finishPrepare() override;
+	virtual void render(const OfferAnswerContext &ctx, CallSession::State targetState) override;
+	virtual void stop() override;
+	virtual void finish() override;
+	virtual ~MS2RTTStream();
+private:
+	void realTimeTextCharacterReceived(MSFilter *f, unsigned int id, void *arg);
+	static void sRealTimeTextCharacterReceived(void *userData, MSFilter *f, unsigned int id, void *arg);
+	virtual MediaStream *getMediaStream()const override;
+	virtual void handleEvent(const OrtpEvent *ev) override;
+	TextStream *mStream = nullptr;
+};
+
+
+LINPHONE_END_NAMESPACE
+
+#endif
+
diff --git a/src/conference/session/port-config.h b/src/conference/session/port-config.h
index 06a2b2b65ed264e941623b438c0b87505113e02a..00f00ccb5c12fd31ae753c4c25e11f32a3c90c54 100644
--- a/src/conference/session/port-config.h
+++ b/src/conference/session/port-config.h
@@ -23,12 +23,14 @@
 #include <string>
 
 #include "linphone/utils/general.h"
+#include "c-wrapper/internal/c-sal.h"
 
 // =============================================================================
 
 LINPHONE_BEGIN_NAMESPACE
 
 struct PortConfig {
+	SalMulticastRole multicastRole = SalMulticastInactive;
 	std::string multicastIp;
 	std::string multicastBindIp;
 	int rtpPort = -1;
diff --git a/src/conference/session/rtt-stream.cpp b/src/conference/session/rtt-stream.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..55591ea25f7e2dc061c3fba6e75ac5d5a2946275
--- /dev/null
+++ b/src/conference/session/rtt-stream.cpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#include "bctoolbox/defs.h"
+
+#include "ms2-streams.h"
+#include "media-session.h"
+#include "media-session-p.h"
+#include "core/core.h"
+#include "c-wrapper/c-wrapper.h"
+#include "call/call.h"
+#include "call/call-p.h"
+#include "conference/participant.h"
+#include "conference/params/media-session-params-p.h"
+
+#include "linphone/core.h"
+
+using namespace ::std;
+
+LINPHONE_BEGIN_NAMESPACE
+
+/*
+ * MS2RTTStream implementation.
+ */
+
+MS2RTTStream::MS2RTTStream(StreamsGroup &sg, const OfferAnswerContext &params) : MS2Stream(sg, params){
+	string bindIp = getBindIp();
+	mStream = text_stream_new2(getCCore()->factory, bindIp.empty() ? nullptr : bindIp.c_str(), mPortConfig.rtpPort, mPortConfig.rtcpPort);
+	initializeSessions(&mStream->ms);
+}
+
+void MS2RTTStream::realTimeTextCharacterReceived (MSFilter *f, unsigned int id, void *arg) {
+	CallSessionListener *listener = getMediaSessionPrivate().getCallSessionListener();
+	
+	if (id == MS_RTT_4103_RECEIVED_CHAR) {
+		RealtimeTextReceivedCharacter *data = static_cast<RealtimeTextReceivedCharacter *>(arg);
+		if (listener)
+			listener->onRealTimeTextCharacterReceived(getMediaSession().getSharedFromThis(), data);
+	}
+}
+
+void MS2RTTStream::sRealTimeTextCharacterReceived (void *userData, MSFilter *f, unsigned int id, void *arg) {
+	MS2RTTStream *zis = static_cast<MS2RTTStream *>(userData);
+	zis->realTimeTextCharacterReceived(f, id, arg);
+}
+
+bool MS2RTTStream::prepare(){
+	MS2Stream::prepare();
+	text_stream_prepare_text(mStream);
+	return false;
+}
+
+void MS2RTTStream::finishPrepare(){
+	MS2Stream::finishPrepare();
+	text_stream_unprepare_text(mStream);
+}
+
+void MS2RTTStream::render(const OfferAnswerContext &params, CallSession::State targetState){
+	const SalStreamDescription *tstream = params.resultStreamDescription;
+	bool basicChangesHandled = handleBasicChanges(params, targetState);
+	
+	if (basicChangesHandled) {
+		if (getState() == Running) MS2Stream::render(params, targetState);
+		return;
+	}
+	
+	MS2Stream::render(params, targetState);
+	RtpAddressInfo dest;
+	getRtpDestination(params, &dest);
+	int usedPt = -1;
+	RtpProfile * textProfile = makeProfile(params.resultMediaDescription, tstream, &usedPt);
+	if (usedPt == -1){
+		lError() << "No payload type was accepted for text stream.";
+		stop();
+		return;
+	}
+	getMediaSessionPrivate().getCurrentParams()->getPrivate()->setUsedRealtimeTextCodec(rtp_profile_get_payload(textProfile, usedPt));
+	getMediaSessionPrivate().getCurrentParams()->enableRealtimeText(true);
+	
+	unsigned int interval = getMediaSessionPrivate().getParams()->realtimeTextKeepaliveInterval();
+	getMediaSessionPrivate().getCurrentParams()->setRealtimeTextKeepaliveInterval(interval);
+	
+	text_stream_start(mStream, textProfile, dest.rtpAddr.c_str(), dest.rtpPort, dest.rtcpAddr.c_str(), dest.rtcpPort, usedPt);
+	ms_filter_add_notify_callback(mStream->rttsink, sRealTimeTextCharacterReceived, this, false);
+	ms_filter_call_method(mStream->rttsource, MS_RTT_4103_SOURCE_SET_KEEP_ALIVE_INTERVAL, &interval);
+	mStartCount++;
+}
+
+void MS2RTTStream::stop(){
+	MS2Stream::stop();
+	text_stream_stop(mStream);
+	/* In mediastreamer2, stop actually stops and destroys. We immediately need to recreate the stream object for later use, keeping the 
+	 * sessions (for RTP, SRTP, ZRTP etc) that were setup at the beginning. */
+	mStream = text_stream_new_with_sessions(getCCore()->factory, &mSessions);
+}
+
+void MS2RTTStream::finish(){
+	if (mStream){
+		text_stream_stop(mStream);
+		mStream = nullptr;
+	}
+}
+
+MS2RTTStream::~MS2RTTStream(){
+	finish();
+}
+
+MediaStream *MS2RTTStream::getMediaStream()const{
+	return &mStream->ms;
+}
+
+void MS2RTTStream::handleEvent(const OrtpEvent *ev){
+}
+
+
+
+
+LINPHONE_END_NAMESPACE
diff --git a/src/conference/session/stream.cpp b/src/conference/session/stream.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bd973f13b56451270eaefefd93aa823110bab189
--- /dev/null
+++ b/src/conference/session/stream.cpp
@@ -0,0 +1,216 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "bctoolbox/defs.h"
+
+#include "streams.h"
+#include "media-session.h"
+#include "media-session-p.h"
+#include "core/core.h"
+#include "c-wrapper/c-wrapper.h"
+#include "call/call.h"
+#include "call/call-p.h"
+#include "conference/participant.h"
+#include "utils/payload-type-handler.h"
+#include "conference/params/media-session-params-p.h"
+
+#include "linphone/core.h"
+
+
+using namespace::std;
+
+LINPHONE_BEGIN_NAMESPACE
+
+
+/*
+ * Stream implementation.
+ */
+
+
+Stream::Stream(StreamsGroup &sg, const OfferAnswerContext &params) : mStreamsGroup(sg), mStreamType(params.localStreamDescription->type), mIndex(params.streamIndex){
+	setPortConfig();
+	fillMulticastMediaAddresses();
+}
+
+void Stream::setMain(){
+	mIsMain = true;
+}
+
+LinphoneCore *Stream::getCCore()const{
+	return getCore().getCCore();
+}
+
+Core &Stream::getCore()const{
+	return *mStreamsGroup.getMediaSession().getCore();
+}
+
+MediaSession &Stream::getMediaSession()const{
+	return mStreamsGroup.getMediaSession();
+}
+
+MediaSessionPrivate &Stream::getMediaSessionPrivate()const{
+	return *getMediaSession().getPrivate();
+}
+
+void Stream::fillLocalMediaDescription(OfferAnswerContext & ctx){
+}
+
+bool Stream::prepare(){
+	mState = Preparing;
+	return false;
+}
+
+void Stream::finishPrepare(){
+	mState = Stopped;
+}
+
+void Stream::tryEarlyMediaForking(const OfferAnswerContext &ctx){
+}
+
+void Stream::render(const OfferAnswerContext & ctx, CallSession::State targetState){
+	mState = Running;
+}
+
+void Stream::sessionConfirmed(const OfferAnswerContext &ctx){
+}
+
+void Stream::stop(){
+	mState = Stopped;
+}
+
+void Stream::setIceCheckList(IceCheckList *cl){
+}
+
+void Stream::iceStateChanged(){
+}
+
+void Stream::setRandomPortConfig () {
+	mPortConfig.rtpPort = -1;
+	mPortConfig.rtcpPort = -1;
+}
+
+int Stream::selectRandomPort (pair<int, int> portRange) {
+	unsigned int rangeSize = static_cast<unsigned int>(portRange.second - portRange.first);
+	
+	for (int nbTries = 0; nbTries < 100; nbTries++) {
+		bool alreadyUsed = false;
+		unsigned int randomInRangeSize = (bctbx_random() % rangeSize) & (unsigned int)~0x1; /* Select an even number */
+		int triedPort = ((int)randomInRangeSize) + portRange.first;
+		/*If portRange.first is even, the triedPort will be even too. The one who configures a port range that starts with an odd number will
+		 * get odd RTP port numbers.*/
+		
+		for (const bctbx_list_t *elem = linphone_core_get_calls(getCCore()); elem != nullptr; elem = bctbx_list_next(elem)) {
+			LinphoneCall *lcall = reinterpret_cast<LinphoneCall *>(bctbx_list_get_data(elem));
+			shared_ptr<MediaSession> session = static_pointer_cast<MediaSession>(L_GET_CPP_PTR_FROM_C_OBJECT(lcall)->getPrivate()->getActiveSession());
+			if (session->getPrivate()->getStreamsGroup().isPortUsed(triedPort)) {
+				alreadyUsed = true;
+				break;
+			}
+		}
+		if (!alreadyUsed){
+			lInfo() << "Port " << triedPort << " randomly taken from range [ " << portRange.first << " , " << portRange.second << "]";
+			return triedPort;
+		}
+	}
+
+	lError() << "Could not find any free port!";
+	return -1;
+}
+
+int Stream::selectFixedPort (pair<int, int> portRange) {
+	for (int triedPort = portRange.first; triedPort < (portRange.first + 100); triedPort += 2) {
+		bool alreadyUsed = false;
+		for (const bctbx_list_t *elem = linphone_core_get_calls(getCCore()); elem != nullptr; elem = bctbx_list_next(elem)) {
+			LinphoneCall *lcall = reinterpret_cast<LinphoneCall *>(bctbx_list_get_data(elem));
+			shared_ptr<MediaSession> session = static_pointer_cast<MediaSession>(L_GET_CPP_PTR_FROM_C_OBJECT(lcall)->getPrivate()->getActiveSession());
+			if (session->getPrivate()->getStreamsGroup().isPortUsed(triedPort)) {
+				alreadyUsed = true;
+				break;
+			}
+		}
+		if (!alreadyUsed)
+			return triedPort;
+	}
+
+	lError() << "Could not find any free port !";
+	return -1;
+}
+
+void Stream::setPortConfig(pair<int, int> portRange) {
+	if ((portRange.first <= 0) && (portRange.second <= 0)) {
+		setRandomPortConfig();
+	} else {
+		if (portRange.first == portRange.second) {
+			/* Fixed port */
+			mPortConfig.rtpPort = selectFixedPort(portRange);
+		} else {
+			/* Select random port in the specified range */
+			mPortConfig.rtpPort = selectRandomPort(portRange);
+		}
+	}
+	if (mPortConfig.rtpPort == -1) setRandomPortConfig();
+	else mPortConfig.rtcpPort = mPortConfig.rtpPort + 1;
+}
+
+void Stream::setPortConfig(){
+	int minPort = 0, maxPort = 0;
+	switch(getType()){
+		case SalAudio:
+			linphone_core_get_audio_port_range(getCCore(), &minPort, &maxPort);
+		break;
+		case SalVideo:
+			linphone_core_get_video_port_range(getCCore(), &minPort, &maxPort);
+		break;
+		case SalText:
+			linphone_core_get_text_port_range(getCCore(), &minPort, &maxPort);
+		break;
+		case SalOther:
+		break;
+	}
+	setPortConfig(make_pair(minPort, maxPort));
+}
+
+void Stream::fillMulticastMediaAddresses () {
+	mPortConfig.multicastIp.clear();
+	if (getType() == SalAudio && getMediaSession().getPrivate()->getParams()->audioMulticastEnabled()){
+		mPortConfig.multicastIp = linphone_core_get_audio_multicast_addr(getCCore());
+	} else if (getType() == SalVideo && getMediaSession().getPrivate()->getParams()->videoMulticastEnabled()){
+		mPortConfig.multicastIp = linphone_core_get_video_multicast_addr(getCCore());
+	}
+}
+
+bool Stream::isPortUsed(int port)const{
+	return port == mPortConfig.rtpPort || port == mPortConfig.rtcpPort;
+}
+
+IceService & Stream::getIceService()const{
+	return mStreamsGroup.getIceService();
+}
+
+const string & Stream::getPublicIp() const{
+	if (!mPortConfig.multicastIp.empty()){
+			return mPortConfig.multicastIp;
+	}
+	return getMediaSessionPrivate().getMediaLocalIp();
+}
+
+void Stream::finish(){
+}
+
+LINPHONE_END_NAMESPACE
diff --git a/src/conference/session/streams-group.cpp b/src/conference/session/streams-group.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..28b6ccfe701df552775c941ffb52b6a745aea98b
--- /dev/null
+++ b/src/conference/session/streams-group.cpp
@@ -0,0 +1,509 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <bctoolbox/defs.h>
+
+#include "streams.h"
+#include "media-session.h"
+#include "media-session-p.h"
+#include "core/core.h"
+#include "c-wrapper/c-wrapper.h"
+#include "call/call.h"
+#include "call/call-p.h"
+#include "conference/participant.h"
+#include "utils/payload-type-handler.h"
+#include "conference/params/media-session-params-p.h"
+#include "nat/ice-service.h"
+#include "linphone/core.h"
+
+#include <iomanip>
+
+using namespace::std;
+
+LINPHONE_BEGIN_NAMESPACE
+
+
+
+/*
+ * StreamsGroup implementation
+ */
+
+StreamsGroup::StreamsGroup(MediaSession &session) : mMediaSession(session){
+	mIceService.reset(new IceService(*this));
+}
+
+StreamsGroup::~StreamsGroup(){
+	finish();
+}
+
+IceService & StreamsGroup::getIceService()const{
+	return *mIceService;
+}
+
+Stream * StreamsGroup::createStream(const OfferAnswerContext &params){
+	Stream *ret = nullptr;
+	SalStreamType type = params.localStreamDescription->type;
+	switch(type){
+		case SalAudio:
+			ret = new MS2AudioStream(*this, params);
+		break;
+		case SalVideo:
+#ifdef VIDEO_ENABLED
+			ret = new MS2VideoStream(*this, params);
+#endif
+		break;
+		case SalText:
+			ret = new MS2RTTStream(*this, params);
+		break;
+		case SalOther:
+		break;
+	}
+	if (!ret){
+		lError() << "Could not create Stream of type " << sal_stream_type_to_string(type);
+		return nullptr;
+	}
+	lInfo() << "Created " << *ret;
+	
+	if ((decltype(mStreams)::size_type)params.streamIndex >= mStreams.size()) mStreams.resize(params.streamIndex + 1);
+	if (mStreams[params.streamIndex] != nullptr){
+		lInfo() << "Stream at index " << params.streamIndex << " is being replaced.";
+	}
+	mStreams[params.streamIndex].reset(ret);
+	return ret;
+}
+
+void StreamsGroup::fillLocalMediaDescription(OfferAnswerContext & params){
+	for (auto &stream : mStreams){
+		params.scopeStreamToIndex(stream->getIndex());
+		stream->fillLocalMediaDescription(params);
+	}
+	mIceService->fillLocalMediaDescription(params);
+}
+
+void StreamsGroup::createStreams(const OfferAnswerContext &params){
+	size_t index;
+	for(index = 0; index < (size_t)params.localMediaDescription->nb_streams; ++index){
+		Stream *s;
+		params.scopeStreamToIndexWithDiff(index, mCurrentOfferAnswerState);
+		
+		if (params.localStreamDescriptionChanges) {
+			char *differences = sal_media_description_print_differences(params.localStreamDescriptionChanges);
+			lInfo() << "Local stream description has changed: " << differences;
+			ms_free(differences);
+		}
+		if (index >= mStreams.size() || (s = mStreams[index].get()) == nullptr){
+			s = createStream(params);
+		}else{
+			if (s->getType() != params.localStreamDescription->type){
+				lError() << "Inconsistency detected while creating streams. Type has changed from " <<
+					sal_stream_type_to_string(s->getType()) << " to " << 
+					sal_stream_type_to_string(params.localStreamDescription->type) << "!";
+			}else if (params.localStreamDescriptionChanges & SAL_MEDIA_DESCRIPTION_NETWORK_XXXCAST_CHANGED ){
+				/*
+				* Special case: due to implementation constraint, it is necessary to instanciate a new Stream when changing 
+				* the cast (uni or multi).
+				*/
+				s->stop();
+				s = createStream(params);
+			}
+		}
+	}
+	mIceService->createStreams(params);
+}
+
+bool StreamsGroup::prepare(){
+	if (mFinished){
+		lError() << "StreamsGroup finished, cannot be used anymore.";
+		return false;
+	}
+	for (auto &stream : mStreams){
+		if (stream->getState() == Stream::Stopped){
+			stream->prepare();
+		}
+	}
+	return mIceService->prepare();
+}
+
+void StreamsGroup::finishPrepare(){
+	for (auto &stream : mStreams){
+		if (stream->getState() == Stream::Preparing){
+			stream->finishPrepare();
+		}
+	}
+	mIceService->finishPrepare();
+}
+
+void StreamsGroup::render(const OfferAnswerContext &constParams, CallSession::State targetState){
+	if (mFinished){
+		lError() << "StreamsGroup finished, cannot be used anymore.";
+		return;
+	}
+	OfferAnswerContext params;
+	params.copyFrom(constParams);
+	
+	if (params.remoteMediaDescription == nullptr){
+		/* This can happen when we receive a 200Ok without SDP, after early media. In this case we use the previously
+		 * provided remote media description.*/
+		params.remoteMediaDescription = mCurrentOfferAnswerState.remoteMediaDescription;
+	}
+	
+	for(auto &stream : mStreams){
+		Stream *streamPtr = stream.get();
+		lInfo() << "StreamsGroup " << this << " rendering " << *stream;
+		params.scopeStreamToIndexWithDiff(stream->getIndex(), mCurrentOfferAnswerState);
+		
+		if (params.localStreamDescriptionChanges) {
+			char *differences = sal_media_description_print_differences(params.localStreamDescriptionChanges);
+			lInfo() << "Local stream description has changed: " << differences;
+			ms_free(differences);
+		}
+		if (params.resultStreamDescriptionChanges) {
+			char *differences = sal_media_description_print_differences(params.resultStreamDescriptionChanges);
+			lInfo() << "Result stream description has changed: " << differences;
+			ms_free(differences);
+		}
+		if (streamPtr->getState() == Stream::Preparing)
+			streamPtr->finishPrepare();
+		streamPtr->render(params, targetState);
+	}
+	if (!mBandwidthReportTimer){
+		mBandwidthReportTimer = getCore().createTimer([this](){ this->computeAndReportBandwidth(); return true; }, 1000 , "StreamsGroup timer");
+	}
+	
+	for(auto &hook : mPostRenderHooks){
+		hook();
+	}
+	mPostRenderHooks.clear();
+	
+	mIceService->render(params, targetState);
+	
+	if (getIceService().hasCompleted()){
+		/* Should not start dtls until ice is completed */
+		startDtls(params);
+	}
+	/* Save the state of the offer-answer, so that we are later able to monitor differences in next render() calls. */
+	mCurrentOfferAnswerState.dupFrom(params);
+}
+
+void StreamsGroup::sessionConfirmed(const OfferAnswerContext &params){
+	for (auto &stream  : mStreams){
+		mCurrentOfferAnswerState.scopeStreamToIndex(stream->getIndex());
+		stream->sessionConfirmed(mCurrentOfferAnswerState);
+	}
+}
+
+void StreamsGroup::stop(){
+	if (mFinished){
+		lError() << "StreamsGroup finished, cannot be used anymore.";
+		abort();
+		return;
+	}
+	if (mBandwidthReportTimer){
+		getCore().destroyTimer(mBandwidthReportTimer);
+		mBandwidthReportTimer = nullptr;
+	}
+	for(auto &stream : mStreams){
+		if (stream && stream->getState() != Stream::Stopped)
+			stream->stop();
+	}
+	mIceService->stop();
+}
+
+Stream * StreamsGroup::getStream(size_t index){
+	if (index >=  mStreams.size()){
+		lFatal() << "Bad stream index " << index;
+		return nullptr;
+	}
+	return mStreams[index].get();
+}
+
+bool StreamsGroup::isPortUsed(int port)const{
+	if (port == -1) return false;
+	for(auto &stream : mStreams){
+		if (stream && stream->isPortUsed(port)) return true;
+	}
+	return false;
+}
+
+LinphoneCore *StreamsGroup::getCCore()const{
+	return mMediaSession.getCore()->getCCore();
+}
+
+Core & StreamsGroup::getCore()const{
+	return *mMediaSession.getCore();
+}
+
+MediaSessionPrivate &StreamsGroup::getMediaSessionPrivate()const{
+	return *getMediaSession().getPrivate();
+}
+
+int StreamsGroup::updateAllocatedAudioBandwidth (const PayloadType *pt, int maxbw) {
+	mAudioBandwidth = PayloadTypeHandler::getAudioPayloadTypeBandwidth(pt, maxbw);
+	lInfo() << "Audio bandwidth for StreamsGroup [" << this << "] is " << mAudioBandwidth;
+	return mAudioBandwidth;
+}
+
+int StreamsGroup::getVideoBandwidth (const SalMediaDescription *md, const SalStreamDescription *desc) {
+	int remoteBandwidth = 0;
+	if (desc->bandwidth > 0)
+		remoteBandwidth = desc->bandwidth;
+	else if (md->bandwidth > 0) {
+		/* Case where b=AS is given globally, not per stream */
+		remoteBandwidth = PayloadTypeHandler::getRemainingBandwidthForVideo(md->bandwidth, mAudioBandwidth);
+	}
+	return PayloadTypeHandler::getMinBandwidth(
+		PayloadTypeHandler::getRemainingBandwidthForVideo(linphone_core_get_upload_bandwidth(getCCore()), mAudioBandwidth), remoteBandwidth);
+}
+
+
+void StreamsGroup::zrtpStarted(Stream *mainZrtpStream){
+	for (auto &stream : mStreams){
+		if (stream && stream.get() != mainZrtpStream) stream->zrtpStarted(mainZrtpStream);
+	}
+	propagateEncryptionChanged();
+}
+
+bool StreamsGroup::allStreamsEncrypted () const {
+	int activeStreamsCount = 0;
+	for (auto &stream : mStreams){
+		if (stream->getState() == Stream::Running){
+			++activeStreamsCount;
+			if (!stream->isEncrypted()){
+				return false;
+			}
+		}
+	}
+	return activeStreamsCount > 0;
+}
+
+
+void StreamsGroup::propagateEncryptionChanged () {
+	getMediaSessionPrivate().propagateEncryptionChanged();
+}
+
+void StreamsGroup::authTokenReady(const string &authToken, bool verified) {
+	mAuthToken = authToken;
+	mAuthTokenVerified = verified;
+	lInfo() << "Authentication token is " << mAuthToken << "(" << (mAuthTokenVerified ? "verified" : "unverified") << ")";
+}
+
+void StreamsGroup::setAuthTokenVerified(bool value){
+	MS2Stream *s = lookupMainStreamInterface<MS2Stream>(SalAudio);
+	if (!s || s->getState() != Stream::Running){
+		lError() << "StreamsGroup::setAuthTokenVerified(): No audio stream or not started";
+		return;
+	}
+	MSZrtpContext *zrtp_context = s->getZrtpContext();
+	if (!zrtp_context) {
+		lError() << "StreamsGroup::setAuthenticationTokenVerified(): No zrtp context";
+		return;
+	}
+	// SAS verified
+	if (value) {
+		ms_zrtp_sas_verified(zrtp_context);
+	} else { // SAS rejected
+		ms_zrtp_sas_reset_verified(zrtp_context);
+	}
+	mAuthTokenVerified = value;
+}
+
+Stream * StreamsGroup::lookupMainStream(SalStreamType type){
+	for (auto &stream : mStreams){
+		if (stream->isMain() && stream->getType() == type){
+			return stream.get();
+		}
+	}
+	return nullptr;
+}
+
+
+void StreamsGroup::tryEarlyMediaForking(const OfferAnswerContext &params) {
+	for (auto & s : mStreams) {
+		params.scopeStreamToIndex(s->getIndex());
+		if (!sal_stream_description_enabled(params.resultStreamDescription) || params.resultStreamDescription->dir == SalStreamInactive)
+			continue;
+		
+		const SalStreamDescription *refStream = params.resultStreamDescription;
+		const SalStreamDescription *newStream = params.remoteStreamDescription;
+		
+		if ((refStream->type == newStream->type) && refStream->payloads && newStream->payloads) {
+			OrtpPayloadType *refpt = static_cast<OrtpPayloadType *>(refStream->payloads->data);
+			OrtpPayloadType *newpt = static_cast<OrtpPayloadType *>(newStream->payloads->data);
+			if ((strcmp(refpt->mime_type, newpt->mime_type) == 0) && (refpt->clock_rate == newpt->clock_rate)
+				&& (payload_type_get_number(refpt) == payload_type_get_number(newpt))) {
+					s->tryEarlyMediaForking(params);
+			}
+		}
+	}
+}
+
+void StreamsGroup::finishEarlyMediaForking(){
+	for (auto &stream : mStreams){
+		if (stream) stream->finishEarlyMediaForking();
+	}
+}
+
+bool StreamsGroup::isStarted()const{
+	for( auto & stream : mStreams){
+		if (stream->getState() == Stream::Running) return true;
+	}
+	return false;
+}
+
+void StreamsGroup::clearStreams(){
+	stop();
+	mIceService.reset(new IceService(*this));
+	mStreams.clear();
+	mCurrentOfferAnswerState.clear();
+}
+
+size_t StreamsGroup::getActiveStreamsCount() const{
+	size_t ret = 0;
+	for( auto & stream : mStreams){
+		if (stream->getState() == Stream::Running) ++ret;
+	}
+	return ret;
+}
+
+bool StreamsGroup::isMuted() const{
+	for (auto & stream : mStreams){
+		if (stream->getState() == Stream::Running){
+			if (stream->isMuted() == false) return false;
+		}
+	}
+	return true;
+}
+
+template< typename _functor>
+float StreamsGroup::computeOverallQuality(_functor func){
+	float globalRating = -1.0f;
+	int countedStreams = 0;
+	for (auto &stream : mStreams){
+		float streamRating = func(stream.get());
+		if (streamRating != -1.0f){
+			if (globalRating == -1.0f){
+				globalRating = streamRating;
+			}else{
+				globalRating += streamRating;
+			}
+			countedStreams++;
+		}
+	}
+	return globalRating / (float)countedStreams;
+}
+
+float StreamsGroup::getAverageQuality(){
+	return computeOverallQuality(mem_fun(&Stream::getAverageQuality));
+}
+
+float StreamsGroup::getCurrentQuality(){
+	return computeOverallQuality(mem_fun(&Stream::getCurrentQuality));
+}
+
+void StreamsGroup::startDtls(const OfferAnswerContext &params){
+	for( auto & stream : mStreams){
+		params.scopeStreamToIndex(stream->getIndex());
+		stream->startDtls(params);
+	}
+}
+
+int StreamsGroup::getAvpfRrInterval()const{
+	int interval = 0;
+	for( auto & stream : mStreams){
+		RtpInterface *i = dynamic_cast<MS2Stream*>(stream.get());
+		if (i && i->getAvpfRrInterval() > interval) 
+			interval = i->getAvpfRrInterval();
+	}
+	return interval;
+}
+
+bool StreamsGroup::avpfEnabled() const{
+	bool ret = false;
+	for( auto & stream : mStreams){
+		RtpInterface *i = dynamic_cast<MS2Stream*>(stream.get());
+		if (i && stream->getState() == Stream::Running){
+			if (!i->avpfEnabled()){
+				return false;
+			}
+			ret = true;
+		}
+	}
+	return ret;
+}
+
+void StreamsGroup::refreshSockets(){
+	forEach<Stream>(mem_fun(&Stream::refreshSockets));
+}
+
+void StreamsGroup::computeAndReportBandwidth(){
+	forEach<Stream>(mem_fun(&Stream::updateBandwidthReports));
+	
+	if (!bctbx_log_level_enabled(BCTBX_LOG_DOMAIN, BCTBX_LOG_MESSAGE)) return;
+	
+	ostringstream ostr;
+	bool introDone = false;
+	
+	for (auto &stream : mStreams){
+		if (!stream) continue;
+		if (stream->getState() != Stream::Running) continue;
+		LinphoneCallStats *stats = stream->getStats();
+		if (!introDone){
+			ostr << "Bandwidth usage for CallSession [" << &getMediaSession() << "]:" << endl << fixed << setprecision(2);
+			introDone = true;
+		}
+		ostr << "\tStream #" << stream->getIndex() << " (" << sal_stream_type_to_string(stream->getType()) << ") | cpu: " << stream->getCpuUsage() << "% |" << " RTP : [d="
+			<< linphone_call_stats_get_download_bandwidth(stats) << ",u=" << linphone_call_stats_get_upload_bandwidth(stats) << "] "
+			<< "RTCP: [d=" << linphone_call_stats_get_rtcp_download_bandwidth(stats) << ",u=" << linphone_call_stats_get_rtcp_upload_bandwidth(stats) << "] ";
+		float est_bw = linphone_call_stats_get_estimated_download_bandwidth(stats);
+		if (est_bw > 0.0) ostr << "Est max d=" << est_bw;
+		ostr << " (kbits/sec)" << endl;
+	}
+	lInfo() << ostr.str();
+}
+
+void StreamsGroup::addPostRenderHook(const std::function<void()> &l){
+	mPostRenderHooks.push_back(l);
+}
+
+void StreamsGroup::setStreamMain(size_t index){
+	Stream *s = getStream(index);
+	if (s){
+		SalStreamType type = s->getType();
+		// Make sure there is not already a "main" stream; which would be a programmer fault.
+		Stream *other = lookupMainStream(type);
+		if (other != nullptr && other != s){
+			lError() << "StreamsGroup::setStreamMain(): error, the main attribute has already been set on another stream.";
+			return;
+		}
+		s->setMain();
+	}
+}
+
+void StreamsGroup::finish(){
+	if (mFinished) return;
+	lInfo() << "StreamsGroup::finish() called.";
+	stop(); //For the paranoid: normally it should be done already.
+	mIceService->finish(); // finish ICE first, as it has actions on the streams.
+	forEach<Stream>(mem_fun(&Stream::finish));
+	mFinished = true;
+}
+
+
+LINPHONE_END_NAMESPACE
+
diff --git a/src/conference/session/streams.h b/src/conference/session/streams.h
new file mode 100644
index 0000000000000000000000000000000000000000..fabb81f72b7e89785cab4ce4b6f185977a5e8e08
--- /dev/null
+++ b/src/conference/session/streams.h
@@ -0,0 +1,386 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef streams_h
+#define streams_h
+
+#include <vector>
+#include <memory>
+
+#include "port-config.h"
+#include "call-session.h"
+#include "media-description-renderer.h"
+
+LINPHONE_BEGIN_NAMESPACE
+
+
+class StreamsGroup;
+class MediaSession;
+class MediaSessionPrivate;
+class MediaSessionParams;
+class IceService;
+
+/**
+ * Base class for any kind of stream that may be setup with SDP.
+ */
+class Stream : public MediaDescriptionRenderer{
+	friend class StreamsGroup;
+public:
+	enum State{
+		Stopped,
+		Preparing,
+		Running
+	};
+	
+	virtual void fillLocalMediaDescription(OfferAnswerContext & ctx) override;
+	/**
+	 * Ask the stream to prepare to run. This may include configuration steps, ICE gathering etc.
+	 */
+	virtual bool prepare() override;
+	
+	/**
+	 * Request the stream to finish the prepare step (such as ICE gathering).
+	 */
+	virtual void finishPrepare() override;
+	/**
+	 * Ask the stream to render according to the supplied offer-answer context and target state.
+	 * render() may be called multiple times according to changes made in the offer answer.
+	 */
+	virtual void render(const OfferAnswerContext & ctx, CallSession::State targetState) override;
+	/**
+	 * Notifies that session is confirmed (called by signaling).
+	 */
+	virtual void sessionConfirmed(const OfferAnswerContext &ctx) override;
+	
+	/**
+	 * Ask the stream to stop. A call to prepare() is necessary before doing a future render() operation, if any.
+	 */
+	virtual void stop() override;
+	
+	/**
+	 * Notifies the stream that it will no longer be used (called in render() ).
+	 * This gives the opportunity to free any useless resource immediately.
+	 * Statistics (LinphoneCallStats ) must remain until destruction.
+	 */
+	virtual void finish() override;
+	virtual LinphoneCallStats *getStats(){
+		return nullptr;
+	}
+	/**
+	 * Called by the IceService to setup the check list to run with the stream.
+	 */
+	virtual void setIceCheckList(IceCheckList *cl);
+	/**
+	 * Called by the IceService to notify the stream of a state change in the ICE check list or the ICE session.
+	 */
+	virtual void iceStateChanged();
+	virtual bool isEncrypted() const = 0;
+	virtual void tryEarlyMediaForking(const OfferAnswerContext &ctx) = 0;
+	virtual void finishEarlyMediaForking() = 0;
+	virtual float getCurrentQuality() = 0;
+	virtual float getAverageQuality() = 0;
+	virtual void startDtls(const OfferAnswerContext &params) = 0;
+	virtual bool isMuted()const = 0;
+	virtual void refreshSockets() = 0;
+	virtual void updateBandwidthReports() = 0;
+	virtual float getCpuUsage()const = 0;
+	size_t getIndex()const { return mIndex; }
+	SalStreamType getType()const{ return mStreamType;}
+	LinphoneCore *getCCore()const;
+	Core &getCore()const;
+	MediaSession &getMediaSession()const;
+	MediaSessionPrivate &getMediaSessionPrivate()const;
+	bool isPortUsed(int port) const;
+	IceService & getIceService()const;
+	State getState()const{ return mState;}
+	StreamsGroup &getGroup()const{ return mStreamsGroup;}
+	// Returns whether this stream is the "main" one of its own type, in constrat to secondary streams.
+	bool isMain()const{ return mIsMain;}
+	int getStartCount()const{ return mStartCount; }
+	const PortConfig &getPortConfig()const{ return mPortConfig; }
+	virtual ~Stream() = default;
+	static std::string stateToString(State st){
+		switch(st){
+			case Stopped:
+				return "Stopped";
+			case Running:
+				return "Running";
+			case Preparing:
+				return "Preparing";
+		}
+		return "undefined";
+	}
+	
+protected:
+	Stream(StreamsGroup &ms, const OfferAnswerContext &params);
+	/**
+	 * Notifies that zrtp primary stream is now secured.
+	 */
+	virtual void zrtpStarted(Stream *mainZrtpStream){};
+	const std::string & getPublicIp() const;
+	PortConfig mPortConfig;
+	int mStartCount = 0; /* The number of time of the underlying stream has been started (or restarted). To be maintained by implementations. */
+private:
+	void setMain();
+	void setPortConfig(std::pair<int, int> portRange);
+	int selectFixedPort(std::pair<int, int> portRange);
+	int selectRandomPort(std::pair<int, int> portRange);
+	void setPortConfig();
+	void setRandomPortConfig();
+	void fillMulticastMediaAddresses();
+	StreamsGroup & mStreamsGroup;
+	const SalStreamType mStreamType;
+	const size_t mIndex;
+	State mState = Stopped;
+	bool mIsMain = false;
+};
+
+inline std::ostream &operator<<(std::ostream & ostr, SalStreamType type){
+	ostr << sal_stream_type_to_string(type);
+	return ostr;
+}
+
+inline std::ostream & operator<<(std::ostream & ostr, const Stream& stream){
+	ostr << "stream#" << stream.getIndex() << " [" << stream.getType() << "] in state [" << Stream::stateToString(stream.getState()) << "]";
+	return ostr;
+}
+
+
+class AudioControlInterface{
+public:
+	virtual void enableMic(bool value) = 0;
+	virtual void enableSpeaker(bool value) = 0;
+	virtual bool micEnabled()const = 0;
+	virtual bool speakerEnabled()const = 0;
+	virtual void startRecording() = 0;
+	virtual void stopRecording() = 0;
+	virtual bool isRecording() = 0;
+	virtual float getPlayVolume() = 0; /* Measured playback volume */
+	virtual float getRecordVolume() = 0; /* Measured record volume */
+	virtual float getMicGain() = 0;
+	virtual void setMicGain(float value) = 0;
+	virtual float getSpeakerGain() = 0;
+	virtual void setSpeakerGain(float value) = 0;
+	virtual void setRoute(LinphoneAudioRoute route) = 0;
+	virtual void sendDtmf(int dtmf) = 0;
+	virtual void enableEchoCancellation(bool value) = 0;
+	virtual bool echoCancellationEnabled()const = 0;
+	virtual ~AudioControlInterface() = default;
+};
+
+class VideoControlInterface{
+public:
+	struct VideoStats{
+		float fps;
+		int width, height;
+	};
+	virtual void sendVfu() = 0;
+	virtual void sendVfuRequest() = 0;
+	virtual void enableCamera(bool value) = 0;
+	virtual bool cameraEnabled() const = 0;
+	virtual void setNativeWindowId(void *w) = 0;
+	virtual void * getNativeWindowId() const = 0;
+	virtual void setNativePreviewWindowId(void *w) = 0;
+	virtual void * getNativePreviewWindowId() const = 0;
+	virtual void parametersChanged() = 0;
+	virtual void requestNotifyNextVideoFrameDecoded () = 0;
+	virtual int takePreviewSnapshot (const std::string& file) = 0;
+	virtual int takeVideoSnapshot (const std::string& file) = 0;
+	virtual void zoomVideo (float zoomFactor, float cx, float cy) = 0;
+	virtual void getRecvStats(VideoStats *s) const = 0;
+	virtual void getSendStats(VideoStats *s) const = 0;
+	virtual ~VideoControlInterface() = default;
+};
+
+/*
+ * Interface to query RTP-related information.
+ */
+class RtpInterface{
+public:
+	virtual bool avpfEnabled() const = 0;
+	virtual bool bundleEnabled() const = 0;
+	virtual int getAvpfRrInterval() const = 0;
+	/*
+	 * Returns true if the stream has its own transport interface.
+	 * This is always true unless rtp bundle mode is on, in which case a stream that is using the transport from another
+	 * stream will return false.
+	 */
+	virtual bool isTransportOwner() const = 0;
+	virtual ~RtpInterface() = default;
+};
+
+
+
+/**
+ * The StreamsGroup takes in charge the initialization and rendering of a group of streams defined
+ * according to a local media description, and a media description resulted from the offer/answer model.
+ * When the offer is received from remote, the local description must be compatible with the remote offer.
+ * The StreamsGroup is not in charge of offer/answer model logic: just the creation, rendering, and destruction of the
+ * streams.
+ */
+class StreamsGroup : public MediaDescriptionRenderer{
+	friend class Stream;
+	friend class MS2Stream;
+	friend class MS2AudioStream;
+public:
+	StreamsGroup(MediaSession &session);
+	~StreamsGroup();
+	/**
+	 * Create the streams according to the specified local and remote description.
+	 * The port and transport addresses are filled into the local description in return.
+	 * The local media description must not be null, the remote media description must not be null only
+	 * when the offer was received from remote side.
+	 */
+	void createStreams(const OfferAnswerContext &params);
+	
+	/**
+	 * Set the "main" attribute to a stream index.
+	 * There can be only one main stream per type (audio, video, text...).
+	 * This attribute is useful to know whether certains tasks must be done on these streams.
+	 */
+	void setStreamMain(size_t index);
+	
+	/**
+	 * Once the streams are created, update the local media description to fill mainly
+	 * transport addresses, which are usually provided by the media layer.
+	 */
+	virtual void fillLocalMediaDescription(OfferAnswerContext & ctx) override;
+	/*
+	 * Request the streams to prepare (configuration steps, ice gathering.
+	 * Returns false if ready, true if prepare() requires more time, in which case 
+	 * ICE events will be submitted to the MediaSession to inform when ready to proceed.
+	 */
+	virtual bool prepare() override;
+	/**
+	 * Request the stream to finish the prepare step (such as ICE gathering).
+	 */
+	virtual void finishPrepare() override;
+	/**
+	 * Render the streams according to the supplied offer answer parameters and target session state.
+	 * Local, remote and result must all be non-null.
+	 */
+	virtual void render(const OfferAnswerContext &params, CallSession::State targetState) override;
+	/**
+	 * Used by signaling to notify that the session is confirmed (typically, when an ACK is received.
+	 */
+	virtual void sessionConfirmed(const OfferAnswerContext &params) override;
+	
+	/**
+	 * Stop streams.
+	 */
+	virtual void stop() override;
+	/**
+	 * Notifies the stream that it will no longer be used (called in render() ).
+	 * This gives the opportunity to free any useless resource immediately.
+	 * Statistics (LinphoneCallStats ) must remain until destruction.
+	 */
+	virtual void finish() override;
+	Stream * getStream(size_t index);
+	Stream * getStream(int index){
+		return getStream((size_t) index);
+	}
+	Stream * lookupMainStream(SalStreamType type);
+	template <typename _interface>
+	_interface * lookupMainStreamInterface(SalStreamType type){
+		Stream *s = lookupMainStream(type);
+		if (s){
+			_interface *iface = dynamic_cast<_interface*>(s);
+			if (iface == nullptr){
+				lError() << "lookupMainStreamInterface(): stream " << s << " cannot be casted to " << typeid(_interface).name();
+			}
+			return iface;
+		}
+		return nullptr;
+	}
+	const std::vector<std::unique_ptr<Stream>> & getStreams(){
+		return mStreams;
+	}
+	MediaSession &getMediaSession()const{
+		return mMediaSession;
+	}
+	bool isPortUsed(int port)const;
+	IceService &getIceService()const;
+	bool allStreamsEncrypted () const;
+	// Returns true if at least one stream was started.
+	bool isStarted()const;
+	// Returns true if all streams are muted (from local source standpoint).
+	bool isMuted() const;
+	// Returns true if all streams have avpf enabled.
+	bool avpfEnabled() const;
+	int getAvpfRrInterval()const;
+	void startDtls(const OfferAnswerContext &params);
+	void tryEarlyMediaForking(const OfferAnswerContext &ctx);
+	void finishEarlyMediaForking();
+	/*
+	 * Iterates over streams, trying to cast them to the _requestedInterface type. If they do cast,
+	 * invoke the lambda expression on them.
+	 */
+	template <typename _requestedInterface, typename _lambda>
+	void forEach(const _lambda &l){
+		for (auto & stream : mStreams){
+			_requestedInterface * iface = dynamic_cast<_requestedInterface*>(stream.get());
+			if (iface) l(iface);
+		}
+	}
+	void clearStreams();
+	float getCurrentQuality();
+	float getAverageQuality();
+	const std::string &getAuthToken()const{ return mAuthToken; };
+	void setAuthTokenVerified(bool value);
+	size_t getActiveStreamsCount() const;
+	size_t size()const{ return mStreams.size(); }
+	void refreshSockets();
+	const std::string & getAuthenticationToken()const{ return mAuthToken; }
+	bool getAuthenticationTokenVerified() const{ return mAuthTokenVerified; }
+	const OfferAnswerContext & getCurrentOfferAnswerContext()const{ return mCurrentOfferAnswerState; };
+	MediaSessionPrivate &getMediaSessionPrivate()const;
+	LinphoneCore *getCCore()const;
+	Core & getCore()const;
+protected:
+	
+	int updateAllocatedAudioBandwidth (const PayloadType *pt, int maxbw);
+	int getVideoBandwidth (const SalMediaDescription *md, const SalStreamDescription *desc);
+	void zrtpStarted(Stream *mainZrtpStream);
+	void propagateEncryptionChanged();
+	void authTokenReady(const std::string &token, bool verified);
+	void addPostRenderHook(const std::function<void()> &l);
+private:
+	template< typename _functor>
+	float computeOverallQuality(_functor func);
+	Stream * createStream(const OfferAnswerContext &param);
+	MediaSession &mMediaSession;
+	std::unique_ptr<IceService> mIceService;
+	std::vector<std::unique_ptr<Stream>> mStreams;
+	void computeAndReportBandwidth();
+	// Upload bandwidth used by audio.
+	int mAudioBandwidth = 0;
+	// Zrtp auth token
+	std::string mAuthToken;
+	belle_sip_source_t *mBandwidthReportTimer = nullptr;
+	std::list<std::function<void()>> mPostRenderHooks;
+	OfferAnswerContext mCurrentOfferAnswerState;
+	bool mAuthTokenVerified = false;
+	bool mFinished = false;
+
+};
+
+LINPHONE_END_NAMESPACE
+
+#endif
+
diff --git a/src/conference/session/tone-manager.cpp b/src/conference/session/tone-manager.cpp
index 3f7ab66084ceaf257340271bae9803322a15e8fc..f5217a47cf5a59a511cd9477affba8fe02df8fc2 100644
--- a/src/conference/session/tone-manager.cpp
+++ b/src/conference/session/tone-manager.cpp
@@ -236,7 +236,7 @@ void ToneManager::createTimerToCleanTonePlayer(unsigned int delay) {
 			return true;
 		};
 
-		mTimer = getCore()->createTimer(callback, delay);
+		mTimer = getCore()->createTimer(callback, delay, "Tone player cleanup");
 	}
 }
 
@@ -406,12 +406,6 @@ void ToneManager::doStartRingbackTone(const std::shared_ptr<CallSession> &sessio
 		return;
 
 	MSSndCard *ringCard = lc->sound_conf.lsd_card ? lc->sound_conf.lsd_card : lc->sound_conf.play_sndcard;
-	int maxRate = std::static_pointer_cast<MediaSession>(session)->getPrivate()->getLocalDesc()->streams[0].max_rate;
-	if (maxRate > 0) ms_snd_card_set_preferred_sample_rate(ringCard, maxRate);
-
-	/* We release sound before playing ringback tone */
-	AudioStream *as = reinterpret_cast<AudioStream *>(std::static_pointer_cast<MediaSession>(session)->getPrivate()->getMediaStream(LinphoneStreamTypeAudio));
-	if (as) audio_stream_unprepare_sound(as);
 
 	if (lc->sound_conf.remote_ring) {
 		ms_snd_card_set_stream_type(ringCard, MS_SND_CARD_STREAM_VOICE);
diff --git a/src/conference/session/video-stream.cpp b/src/conference/session/video-stream.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0d9051a61fde9ca9e6c6082e62ed40cb850d8765
--- /dev/null
+++ b/src/conference/session/video-stream.cpp
@@ -0,0 +1,513 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#ifdef VIDEO_ENABLED
+
+#include "bctoolbox/defs.h"
+
+#include "ms2-streams.h"
+#include "media-session.h"
+#include "media-session-p.h"
+#include "core/core.h"
+#include "c-wrapper/c-wrapper.h"
+#include "call/call.h"
+#include "call/call-p.h"
+#include "conference/participant.h"
+#include "conference/params/media-session-params-p.h"
+
+#include "mediastreamer2/msjpegwriter.h"
+#include "mediastreamer2/msogl.h"
+
+#include "linphone/core.h"
+
+using namespace::std;
+
+LINPHONE_BEGIN_NAMESPACE
+
+/*
+ * MS2VideoStream implemenation
+ */
+
+MS2VideoStream::MS2VideoStream(StreamsGroup &sg, const OfferAnswerContext &params) : MS2Stream(sg, params){
+	string bindIp = getBindIp();
+	mStream = video_stream_new2(getCCore()->factory, bindIp.empty() ? nullptr : bindIp.c_str(), mPortConfig.rtpPort, mPortConfig.rtcpPort);
+	initializeSessions(&mStream->ms);
+}
+
+void MS2VideoStream::sVideoStreamEventCb (void *userData, const MSFilter *f, const unsigned int eventId, const void *args) {
+	MS2VideoStream *zis = static_cast<MS2VideoStream*>(userData);
+	zis->videoStreamEventCb(f, eventId, args);
+}
+
+
+void MS2VideoStream::videoStreamEventCb (const MSFilter *f, const unsigned int eventId, const void *args) {
+	CallSessionListener *listener = getMediaSessionPrivate().getCallSessionListener();
+	
+	switch (eventId) {
+		case MS_VIDEO_DECODER_DECODING_ERRORS:
+			lWarning() << "MS_VIDEO_DECODER_DECODING_ERRORS";
+			if (mStream && video_stream_is_decoding_error_to_be_reported(mStream, 5000)) {
+				video_stream_decoding_error_reported(mStream);
+				sendVfu();
+			}
+			break;
+		case MS_VIDEO_DECODER_RECOVERED_FROM_ERRORS:
+			lInfo() << "MS_VIDEO_DECODER_RECOVERED_FROM_ERRORS";
+			if (mStream)
+				video_stream_decoding_error_recovered(mStream);
+			break;
+		case MS_VIDEO_DECODER_FIRST_IMAGE_DECODED:
+			lInfo() << "First video frame decoded successfully";
+			if (listener)
+				listener->onFirstVideoFrameDecoded(getMediaSession().getSharedFromThis());
+			break;
+		case MS_VIDEO_DECODER_SEND_PLI:
+		case MS_VIDEO_DECODER_SEND_SLI:
+		case MS_VIDEO_DECODER_SEND_RPSI:
+			/* Handled internally by mediastreamer2 */
+			break;
+		case MS_CAMERA_PREVIEW_SIZE_CHANGED: {
+			MSVideoSize size = *(MSVideoSize *)args;
+			lInfo() << "Camera video preview size changed: " << size.width << "x" << size.height;
+			linphone_core_resize_video_preview(getCCore(), size.width, size.height);
+			break;
+		}
+		default:
+			lWarning() << "Unhandled event " << eventId;
+			break;
+	}
+}
+
+MediaStream *MS2VideoStream::getMediaStream()const{
+	return &mStream->ms;
+}
+
+void MS2VideoStream::sendVfu(){
+	video_stream_send_vfu(mStream);
+}
+
+void MS2VideoStream::sendVfuRequest(){
+	video_stream_send_fir(mStream);
+}
+
+void MS2VideoStream::zoomVideo (float zoomFactor, float cx, float cy){
+	if (mStream->output) {
+		if (zoomFactor < 1)
+			zoomFactor = 1;
+		float halfsize = 0.5f * 1.0f / zoomFactor;
+		if ((cx - halfsize) < 0)
+			cx = 0 + halfsize;
+		if ((cx + halfsize) > 1)
+			cx = 1 - halfsize;
+		if ((cy - halfsize) < 0)
+			cy = 0 + halfsize;
+		if ((cy + halfsize) > 1)
+			cy = 1 - halfsize;
+		float zoom[3] = { zoomFactor, cx, cy };
+		ms_filter_call_method(mStream->output, MS_VIDEO_DISPLAY_ZOOM, &zoom);
+	} else
+		lWarning() << "Could not apply zoom: video output wasn't activated";
+}
+
+void MS2VideoStream::parametersChanged(){
+	if (getState() != Stream::Running) return;
+	const LinphoneVideoDefinition *vdef = linphone_core_get_preferred_video_definition(getCCore());
+	MSVideoSize vsize;
+	vsize.width = static_cast<int>(linphone_video_definition_get_width(vdef));
+	vsize.height = static_cast<int>(linphone_video_definition_get_height(vdef));
+	video_stream_set_sent_video_size(mStream, vsize);
+	video_stream_set_fps(mStream, linphone_core_get_preferred_framerate(getCCore()));
+	if (mCameraEnabled && (mStream->cam != getCCore()->video_conf.device))
+		video_stream_change_camera(mStream, getCCore()->video_conf.device);
+	else
+		video_stream_update_video_params(mStream);
+}
+
+void MS2VideoStream::setNativeWindowId(void *w){
+	mNativeWindowId = w;
+	video_stream_set_native_window_id(mStream, w);
+}
+
+void * MS2VideoStream::getNativeWindowId() const{
+	if (mNativeWindowId){
+		return mNativeWindowId;
+	}
+	/* It was not set but we want to get the one automatically created by mediastreamer2 (desktop versions only) */
+	return video_stream_get_native_window_id(mStream);
+}
+
+void MS2VideoStream::setNativePreviewWindowId(void *w){
+	mNativePreviewWindowId = w;
+	video_stream_set_native_preview_window_id(mStream, w);
+}
+
+void * MS2VideoStream::getNativePreviewWindowId() const{
+	return mNativePreviewWindowId;
+}
+
+void MS2VideoStream::enableCamera(bool value){
+	mCameraEnabled = value;
+	MSWebCam *videoDevice = getVideoDevice(getMediaSession().getState());
+	if (video_stream_started(mStream) && (video_stream_get_camera(mStream) != videoDevice)) {
+		string currentCam = video_stream_get_camera(mStream) ? ms_web_cam_get_name(video_stream_get_camera(mStream)) : "NULL";
+		string newCam = videoDevice ? ms_web_cam_get_name(videoDevice) : "NULL";
+		lInfo() << "Switching video cam from [" << currentCam << "] to [" << newCam << "]";
+		video_stream_change_camera(mStream, videoDevice);
+	}
+}
+
+MSWebCam * MS2VideoStream::getVideoDevice(CallSession::State targetState) const {
+	bool paused = (targetState == CallSession::State::Pausing) || (targetState == CallSession::State::Paused);
+	if (paused || mMuted || !mCameraEnabled)
+		return ms_web_cam_manager_get_cam(ms_factory_get_web_cam_manager(getCCore()->factory),
+			"StaticImage: Static picture");
+	else
+		return getCCore()->video_conf.device;
+}
+
+void MS2VideoStream::activateZrtp(){
+	if (linphone_core_media_encryption_supported(getCCore(), LinphoneMediaEncryptionZRTP)){
+		Stream *audioStream = getGroup().lookupMainStream(SalAudio);
+		if (audioStream){
+			MS2AudioStream *msa = dynamic_cast<MS2AudioStream*>(audioStream);
+			video_stream_enable_zrtp(mStream, (AudioStream*)msa->getMediaStream());
+			// Since the zrtp session is now initialized, make sure it is retained for future use.
+			media_stream_reclaim_sessions((MediaStream*)mStream, &mSessions);
+			video_stream_start_zrtp(mStream);
+		}else{
+			lError() << "Error while enabling zrtp on video stream: the audio stream isn't known. This is unsupported.";
+		}
+	}
+}
+
+
+bool MS2VideoStream::prepare(){
+	
+	MS2Stream::prepare();
+	video_stream_prepare_video(mStream);
+	return false;
+}
+
+void MS2VideoStream::finishPrepare(){
+	MS2Stream::finishPrepare();
+	video_stream_unprepare_video(mStream);
+}
+
+void MS2VideoStream::render(const OfferAnswerContext & ctx, CallSession::State targetState){
+	bool reusedPreview = false;
+	CallSessionListener *listener = getMediaSessionPrivate().getCallSessionListener();
+	
+	/* Shutdown preview */
+	MSFilter *source = nullptr;
+	if (getCCore()->previewstream) {
+		if (getCCore()->video_conf.reuse_preview_source)
+			source = video_preview_stop_reuse_source(getCCore()->previewstream);
+		else
+			video_preview_stop(getCCore()->previewstream);
+		getCCore()->previewstream = nullptr;
+	}
+	const SalStreamDescription *vstream = ctx.resultStreamDescription;
+	
+	bool basicChangesHandled = handleBasicChanges(ctx, targetState);
+	
+	if (basicChangesHandled) {
+		bool muted = mMuted;
+		if (getState() == Running) {
+			MS2Stream::render(ctx, targetState); // MS2Stream::render() may decide to unmute.
+			if (muted && !mMuted) {
+				lInfo() << "Early media finished, unmuting video input...";
+				/* We were in early media, now we want to enable real media */
+				mMuted = false;
+				enableCamera(mCameraEnabled);
+			}
+		}
+		return;
+	}
+
+	int usedPt = -1;
+	RtpProfile *videoProfile = makeProfile(ctx.resultMediaDescription, vstream, &usedPt);
+	if (usedPt == -1){
+		lError() << "No payload types accepted for video stream !";
+		stop();
+		return;
+	}
+	
+	
+	video_stream_enable_display_filter_auto_rotate(mStream,
+		!!lp_config_get_int(linphone_core_get_config(getCCore()), "video", "display_filter_auto_rotate", 0)
+	);
+
+	const char *displayFilter = linphone_core_get_video_display_filter(getCCore());
+	if (displayFilter)
+		video_stream_set_display_filter_name(mStream, displayFilter);
+	video_stream_set_event_callback(mStream, sVideoStreamEventCb, this);
+	if (isMain()){
+		getMediaSessionPrivate().getCurrentParams()->getPrivate()->setUsedVideoCodec(rtp_profile_get_payload(videoProfile, usedPt));
+	}
+
+	if (getCCore()->video_conf.preview_vsize.width != 0)
+		video_stream_set_preview_size(mStream, getCCore()->video_conf.preview_vsize);
+	video_stream_set_fps(mStream, linphone_core_get_preferred_framerate(getCCore()));
+	if (lp_config_get_int(linphone_core_get_config(getCCore()), "video", "nowebcam_uses_normal_fps", 0))
+		mStream->staticimage_webcam_fps_optimization = false;
+	const LinphoneVideoDefinition *vdef = linphone_core_get_preferred_video_definition(getCCore());
+	MSVideoSize vsize;
+	vsize.width = static_cast<int>(linphone_video_definition_get_width(vdef));
+	vsize.height = static_cast<int>(linphone_video_definition_get_height(vdef));
+	video_stream_set_sent_video_size(mStream, vsize);
+	video_stream_enable_self_view(mStream, getCCore()->video_conf.selfview);
+	if (mNativeWindowId)
+		video_stream_set_native_window_id(mStream, mNativeWindowId);
+	else if (getCCore()->video_window_id)
+		video_stream_set_native_window_id(mStream, getCCore()->video_window_id);
+	if (getCCore()->preview_window_id)
+		video_stream_set_native_preview_window_id(mStream, getCCore()->preview_window_id);
+	video_stream_use_preview_video_window(mStream, getCCore()->use_preview_window);
+	
+	MS2Stream::render(ctx, targetState);
+	
+	RtpAddressInfo dest;
+	getRtpDestination(ctx, &dest);
+	MediaStreamDir dir = MediaStreamSendRecv;
+		
+	if ((vstream->dir == SalStreamSendOnly) && getCCore()->video_conf.capture)
+		dir = MediaStreamSendOnly;
+	else if ((vstream->dir == SalStreamRecvOnly) && getCCore()->video_conf.display)
+		dir = MediaStreamRecvOnly;
+	else if (vstream->dir == SalStreamSendRecv) {
+		if (getCCore()->video_conf.display && getCCore()->video_conf.capture)
+			dir = MediaStreamSendRecv;
+		else if (getCCore()->video_conf.display)
+			dir = MediaStreamRecvOnly;
+		else
+			dir = MediaStreamSendOnly;
+	}else {
+		lWarning() << "Video stream is inactive";
+		/* Either inactive or incompatible with local capabilities */
+		stop();
+		return;
+	}
+	if (vstream->multicast_role == SalMulticastReceiver){
+			dir = MediaStreamRecvOnly;
+	}else if (vstream->multicast_role == SalMulticastSender){
+		dir = MediaStreamSendOnly;
+	}
+	
+	MSWebCam *cam = getVideoDevice(targetState);
+	
+	getMediaSession().getLog()->video_enabled = true;
+	video_stream_set_direction(mStream, dir);
+	lInfo() << "Device rotation =" << getCCore()->device_rotation;
+	video_stream_set_device_rotation(mStream, getCCore()->device_rotation);
+	video_stream_set_freeze_on_error(mStream, !!lp_config_get_int(linphone_core_get_config(getCCore()), "video", "freeze_on_error", 1));
+	video_stream_use_video_preset(mStream, lp_config_get_string(linphone_core_get_config(getCCore()), "video", "preset", nullptr));
+	if (getCCore()->video_conf.reuse_preview_source && source) {
+		lInfo() << "video_stream_start_with_source kept: " << source;
+		video_stream_start_with_source(mStream, videoProfile, dest.rtpAddr.c_str(), dest.rtpPort, dest.rtcpAddr.c_str(),
+			dest.rtcpPort,
+			usedPt, -1, cam, source);
+		reusedPreview = true;
+	} else {
+		bool ok = true;
+		MSMediaStreamIO io = MS_MEDIA_STREAM_IO_INITIALIZER;
+		if (linphone_config_get_bool(linphone_core_get_config(getCCore()), "video", "rtp_io", FALSE)) {
+			io.input.type = io.output.type = MSResourceRtp;
+			io.input.session = io.output.session = createRtpIoSession();
+			if (!io.input.session) {
+				ok = false;
+				lWarning() << "Cannot create video RTP IO session";
+			}
+		} else {
+			io.input.type = MSResourceCamera;
+			io.input.camera = cam;
+			io.output.type = MSResourceDefault;
+		}
+		if (ok) {
+			AudioStream *as = getPeerAudioStream();
+			if (as) audio_stream_link_video(as, mStream);
+			video_stream_start_from_io(mStream, videoProfile, dest.rtpAddr.c_str(), dest.rtpPort, dest.rtcpAddr.c_str(), dest.rtcpPort,
+				usedPt, &io);
+		}
+	}
+	mStartCount++;
+
+	if (listener)
+		listener->onResetFirstVideoFrameDecoded(getMediaSession().getSharedFromThis());
+	/* Start ZRTP engine if needed : set here or remote have a zrtp-hash attribute */
+	const SalStreamDescription *remoteStream = ctx.remoteStreamDescription;
+	if ((getMediaSessionPrivate().getParams()->getMediaEncryption() == LinphoneMediaEncryptionZRTP) || (remoteStream->haveZrtpHash == 1)) {
+		Stream *audioStream = getGroup().lookupMainStream(SalAudio);
+		/* Audio stream is already encrypted and video stream is active */
+		if (audioStream && audioStream->isEncrypted()) {
+			activateZrtp();
+			if (remoteStream->haveZrtpHash == 1) {
+				int retval = ms_zrtp_setPeerHelloHash(mSessions.zrtp_context, (uint8_t *)remoteStream->zrtphash, strlen((const char *)(remoteStream->zrtphash)));
+				if (retval != 0)
+					lError() << "Video stream ZRTP hash mismatch 0x" << hex << retval;
+			}
+		}
+	}
+
+	if (linphone_core_retransmission_on_nack_enabled(getCCore())) {
+		video_stream_enable_retransmission_on_nack(mStream, TRUE);
+	}
+	
+	if (!reusedPreview && source) {
+		/* Destroy not-reused source filter */
+		lWarning() << "Video preview (" << source << ") not reused: destroying it";
+		ms_filter_destroy(source);
+	}
+	
+}
+
+void MS2VideoStream::stop(){
+	MS2Stream::stop();
+	AudioStream *as = getPeerAudioStream();
+	if (as) audio_stream_unlink_video(as, mStream);
+	video_stream_stop(mStream);
+	/* In mediastreamer2, stop actually stops and destroys. We immediately need to recreate the stream object for later use, keeping the 
+	 * sessions (for RTP, SRTP, ZRTP etc) that were setup at the beginning. */
+	mStream = video_stream_new_with_sessions(getCCore()->factory, &mSessions);
+	getMediaSessionPrivate().getCurrentParams()->getPrivate()->setUsedVideoCodec(nullptr);
+}
+
+void MS2VideoStream::handleEvent(const OrtpEvent *ev){
+	OrtpEventType evt = ortp_event_get_type(ev);
+	OrtpEventData *evd = ortp_event_get_data(const_cast<OrtpEvent*>(ev));
+	
+	if (evt == ORTP_EVENT_NEW_VIDEO_BANDWIDTH_ESTIMATION_AVAILABLE) {
+		lInfo() << "Video bandwidth estimation is " << (int)(evd->info.video_bandwidth_available / 1000.) << " kbit/s";
+		if (isMain())
+			linphone_call_stats_set_estimated_download_bandwidth(mStats, (float)(evd->info.video_bandwidth_available*1e-3));
+	}
+}
+
+void MS2VideoStream::zrtpStarted(Stream *mainZrtpStream){
+	if (getState() == Running){
+		lInfo() << "Trying to start ZRTP encryption on video stream";
+		activateZrtp();
+		if (getMediaSessionPrivate().isEncryptionMandatory()) {
+			/* Nothing could have been sent yet so generating key frame */
+			video_stream_send_vfu(mStream);
+		}
+	}
+}
+
+void MS2VideoStream::tryEarlyMediaForking(const OfferAnswerContext &ctx){
+	MS2Stream::tryEarlyMediaForking(ctx);
+	sendVfu();
+}
+
+void MS2VideoStream::oglRender(){
+	if (mStream->output && (ms_filter_get_id(mStream->output) == MS_OGL_ID))
+		ms_filter_call_method(mStream->output, MS_OGL_RENDER, nullptr);
+}
+
+AudioStream *MS2VideoStream::getPeerAudioStream(){
+	MS2AudioStream *as = getGroup().lookupMainStreamInterface<MS2AudioStream>(SalAudio);
+	return as ? (AudioStream*)as->getMediaStream() : nullptr;
+}
+
+void MS2VideoStream::requestNotifyNextVideoFrameDecoded () {
+	if (mStream->ms.decoder)
+		ms_filter_call_method_noarg(mStream->ms.decoder, MS_VIDEO_DECODER_RESET_FIRST_IMAGE_NOTIFICATION);
+}
+
+
+void MS2VideoStream::snapshotTakenCb(void *userdata, struct _MSFilter *f, unsigned int id, void *arg) {
+	if (id == MS_JPEG_WRITER_SNAPSHOT_TAKEN) {
+		CallSessionListener *listener = getMediaSessionPrivate().getCallSessionListener();
+		const char *filepath = (const char *) arg;
+		listener->onSnapshotTaken(getMediaSession().getSharedFromThis(), filepath);
+	}
+}
+
+void MS2VideoStream::sSnapshotTakenCb(void *userdata, struct _MSFilter *f, unsigned int id, void *arg) {
+	MS2VideoStream *d = (MS2VideoStream *)userdata;
+	d->snapshotTakenCb(userdata, f, id, arg);
+}
+
+int MS2VideoStream::takePreviewSnapshot (const string& file) {
+	if (mStream && mStream->local_jpegwriter) {
+		ms_filter_clear_notify_callback(mStream->jpegwriter);
+		const char *filepath = file.empty() ? nullptr : file.c_str();
+		ms_filter_add_notify_callback(mStream->local_jpegwriter, sSnapshotTakenCb, this, TRUE);
+		return ms_filter_call_method(mStream->local_jpegwriter, MS_JPEG_WRITER_TAKE_SNAPSHOT, (void *)filepath);
+	}
+	lWarning() << "Cannot take local snapshot: no currently running video stream on this call";
+	return -1;
+}
+
+int MS2VideoStream::takeVideoSnapshot (const string& file) {
+	if (mStream && mStream->jpegwriter) {
+		ms_filter_clear_notify_callback(mStream->jpegwriter);
+		const char *filepath = file.empty() ? nullptr : file.c_str();
+		ms_filter_add_notify_callback(mStream->jpegwriter, sSnapshotTakenCb, this, TRUE);
+		return ms_filter_call_method(mStream->jpegwriter, MS_JPEG_WRITER_TAKE_SNAPSHOT, (void *)filepath);
+	}
+	lWarning() << "Cannot take snapshot: no currently running video stream on this call";
+	return -1;
+}
+
+bool MS2VideoStream::cameraEnabled() const{
+	return mCameraEnabled;
+}
+
+void MS2VideoStream::getRecvStats(VideoStats *s) const{
+	if (mStream){
+		s->fps = video_stream_get_received_framerate(mStream);
+		MSVideoSize vsize = video_stream_get_received_video_size(mStream);
+		s->width = vsize.width;
+		s->height = vsize.height;
+	}else{
+		s->fps = 0.0;
+		s->width = s->height = 0;
+	}
+}
+
+void MS2VideoStream::getSendStats(VideoStats *s) const{
+	if (mStream){
+		s->fps = video_stream_get_sent_framerate(mStream);
+		MSVideoSize vsize = video_stream_get_sent_video_size(mStream);
+		s->width = vsize.width;
+		s->height = vsize.height;
+	}else{
+		s->fps = 0.0;
+		s->width = s->height = 0;
+	}
+}
+
+void MS2VideoStream::finish(){
+	if (mStream) {
+		video_stream_stop(mStream);
+		mStream = nullptr;
+	}
+	MS2Stream::finish();
+}
+
+MS2VideoStream::~MS2VideoStream(){
+	if (mStream) video_stream_stop(mStream);
+}
+
+LINPHONE_END_NAMESPACE
+
+#endif
+
+
diff --git a/src/core/core-call.cpp b/src/core/core-call.cpp
index 65f705456468be0512248416941e40ae00a9792a..53447eb540fe3e51b4e07eee4fbc7f162b0625bf 100644
--- a/src/core/core-call.cpp
+++ b/src/core/core-call.cpp
@@ -23,6 +23,7 @@
 #include "core-p.h"
 #include "call/call-p.h"
 #include "conference/session/call-session-p.h"
+#include "conference/session/media-session.h"
 #include "logger/logger.h"
 
 // TODO: Remove me later.
@@ -110,120 +111,21 @@ int CorePrivate::removeCall (const shared_ptr<Call> &call) {
 	return 0;
 }
 
-void CorePrivate::unsetVideoWindowId (bool preview, void *id) {
+void CorePrivate::setVideoWindowId (bool preview, void *id) {
 #ifdef VIDEO_ENABLED
 	for (const auto &call : calls) {
-		VideoStream *vstream = reinterpret_cast<VideoStream *>(call->getPrivate()->getMediaStream(LinphoneStreamTypeVideo));
-		if (vstream) {
-			if (preview)
-				video_stream_set_native_preview_window_id(vstream, id);
-			else
-				video_stream_set_native_window_id(vstream, id);
-		}
-	}
-#endif
-}
-
-// -----------------------------------------------------------------------------
-
-void CorePrivate::parameterizeEqualizer (AudioStream *stream) {
-	L_Q();
-	LinphoneConfig *config = linphone_core_get_config(q->getCCore());
-	const char *eqActive = lp_config_get_string(config, "sound", "eq_active", nullptr);
-	if (eqActive)
-		lWarning() << "'eq_active' linphonerc parameter has no effect anymore. Please use 'mic_eq_active' or 'spk_eq_active' instead";
-	const char *eqGains = lp_config_get_string(config, "sound", "eq_gains", nullptr);
-	if(eqGains)
-		lWarning() << "'eq_gains' linphonerc parameter has no effect anymore. Please use 'mic_eq_gains' or 'spk_eq_gains' instead";
-	if (stream->mic_equalizer) {
-		MSFilter *f = stream->mic_equalizer;
-		bool enabled = !!lp_config_get_int(config, "sound", "mic_eq_active", 0);
-		ms_filter_call_method(f, MS_EQUALIZER_SET_ACTIVE, &enabled);
-		const char *gains = lp_config_get_string(config, "sound", "mic_eq_gains", nullptr);
-		if (enabled && gains) {
-			bctbx_list_t *gainsList = ms_parse_equalizer_string(gains);
-			for (bctbx_list_t *it = gainsList; it; it = bctbx_list_next(it)) {
-				MSEqualizerGain *g = reinterpret_cast<MSEqualizerGain *>(bctbx_list_get_data(it));
-				lInfo() << "Read microphone equalizer gains: " << g->frequency << "(~" << g->width << ") --> " << g->gain;
-				ms_filter_call_method(f, MS_EQUALIZER_SET_GAIN, g);
+		shared_ptr<MediaSession> ms = dynamic_pointer_cast<MediaSession>(call->getPrivate()->getActiveSession());
+		if (ms){
+			if (preview){
+				ms->setNativePreviewWindowId(id);
+			}else{
+				ms->setNativeVideoWindowId(id);
 			}
-			if (gainsList)
-				bctbx_list_free_with_data(gainsList, ms_free);
 		}
 	}
-	if (stream->spk_equalizer) {
-		MSFilter *f = stream->spk_equalizer;
-		bool enabled = !!lp_config_get_int(config, "sound", "spk_eq_active", 0);
-		ms_filter_call_method(f, MS_EQUALIZER_SET_ACTIVE, &enabled);
-		const char *gains = lp_config_get_string(config, "sound", "spk_eq_gains", nullptr);
-		if (enabled && gains) {
-			bctbx_list_t *gainsList = ms_parse_equalizer_string(gains);
-			for (bctbx_list_t *it = gainsList; it; it = bctbx_list_next(it)) {
-				MSEqualizerGain *g = reinterpret_cast<MSEqualizerGain *>(bctbx_list_get_data(it));
-				lInfo() << "Read speaker equalizer gains: " << g->frequency << "(~" << g->width << ") --> " << g->gain;
-				ms_filter_call_method(f, MS_EQUALIZER_SET_GAIN, g);
-			}
-			if (gainsList)
-				bctbx_list_free_with_data(gainsList, ms_free);
-		}
-	}
-}
-
-void CorePrivate::postConfigureAudioStream (AudioStream *stream, bool muted) {
-	L_Q();
-	float micGain = q->getCCore()->sound_conf.soft_mic_lev;
-	if (muted)
-		audio_stream_set_mic_gain(stream, 0);
-	else
-		audio_stream_set_mic_gain_db(stream, micGain);
-	float recvGain = q->getCCore()->sound_conf.soft_play_lev;
-	if (static_cast<int>(recvGain))
-		setPlaybackGainDb(stream, recvGain);
-	LinphoneConfig *config = linphone_core_get_config(q->getCCore());
-	float ngThres = lp_config_get_float(config, "sound", "ng_thres", 0.05f);
-	float ngFloorGain = lp_config_get_float(config, "sound", "ng_floorgain", 0);
-	if (stream->volsend) {
-		int dcRemoval = lp_config_get_int(config, "sound", "dc_removal", 0);
-		ms_filter_call_method(stream->volsend, MS_VOLUME_REMOVE_DC, &dcRemoval);
-		float speed = lp_config_get_float(config, "sound", "el_speed", -1);
-		float thres = lp_config_get_float(config, "sound", "el_thres", -1);
-		float force = lp_config_get_float(config, "sound", "el_force", -1);
-		int sustain = lp_config_get_int(config, "sound", "el_sustain", -1);
-		float transmitThres = lp_config_get_float(config, "sound", "el_transmit_thres", -1);
-		if (static_cast<int>(speed) == -1)
-			speed = 0.03f;
-		if (static_cast<int>(force) == -1)
-			force = 25;
-		MSFilter *f = stream->volsend;
-		ms_filter_call_method(f, MS_VOLUME_SET_EA_SPEED, &speed);
-		ms_filter_call_method(f, MS_VOLUME_SET_EA_FORCE, &force);
-		if (static_cast<int>(thres) != -1)
-			ms_filter_call_method(f, MS_VOLUME_SET_EA_THRESHOLD, &thres);
-		if (static_cast<int>(sustain) != -1)
-			ms_filter_call_method(f, MS_VOLUME_SET_EA_SUSTAIN, &sustain);
-		if (static_cast<int>(transmitThres) != -1)
-			ms_filter_call_method(f, MS_VOLUME_SET_EA_TRANSMIT_THRESHOLD, &transmitThres);
-		ms_filter_call_method(f, MS_VOLUME_SET_NOISE_GATE_THRESHOLD, &ngThres);
-		ms_filter_call_method(f, MS_VOLUME_SET_NOISE_GATE_FLOORGAIN, &ngFloorGain);
-	}
-	if (stream->volrecv) {
-		/* Parameters for a limited noise-gate effect, using echo limiter threshold */
-		float floorGain = (float)(1 / pow(10, micGain / 10));
-		int spkAgc = lp_config_get_int(config, "sound", "speaker_agc_enabled", 0);
-		MSFilter *f = stream->volrecv;
-		ms_filter_call_method(f, MS_VOLUME_ENABLE_AGC, &spkAgc);
-		ms_filter_call_method(f, MS_VOLUME_SET_NOISE_GATE_THRESHOLD, &ngThres);
-		ms_filter_call_method(f, MS_VOLUME_SET_NOISE_GATE_FLOORGAIN, &floorGain);
-	}
-	parameterizeEqualizer(stream);
+#endif
 }
 
-void CorePrivate::setPlaybackGainDb (AudioStream *stream, float gain) {
-	if (stream->volrecv)
-		ms_filter_call_method(stream->volrecv, MS_VOLUME_SET_DB_GAIN, &gain);
-	else
-		lWarning() << "Could not apply playback gain: gain control wasn't activated";
-}
 
 // =============================================================================
 
diff --git a/src/core/core-p.h b/src/core/core-p.h
index e544c863d34d904d26b1a86b6bccfa6363b891e5..3e8bd67b3cff99204faba8c61940b6ca3f091b2b 100644
--- a/src/core/core-p.h
+++ b/src/core/core-p.h
@@ -73,11 +73,7 @@ public:
 	void notifySoundcardUsage (bool used);
 	int removeCall (const std::shared_ptr<Call> &call);
 	void setCurrentCall (const std::shared_ptr<Call> &call) { currentCall = call; }
-	void unsetVideoWindowId (bool preview, void *id);
-
-	void parameterizeEqualizer (AudioStream *stream);
-	void postConfigureAudioStream (AudioStream *stream, bool muted);
-	void setPlaybackGainDb (AudioStream *stream, float gain);
+	void setVideoWindowId (bool preview, void *id);
 
 	void loadChatRooms ();
 	void handleEphemeralMessages (time_t currentTime);
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 29c5f97bd849cc9663a60b1cbb957c4d86c7e673..e5b056f0eaf868a3c20daf9adab8509a433cc49d 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -656,9 +656,10 @@ void Core::doLater(const std::function<void ()> &something){
 	getPrivate()->doLater(something);
 }
 
-belle_sip_source_t *Core::createTimer(const std::function<bool ()> &something, unsigned int milliseconds){
-	return belle_sip_main_loop_create_cpp_timeout_2(getPrivate()->getMainLoop(), something, milliseconds, "");
+belle_sip_source_t *Core::createTimer(const std::function<bool ()> &something, int milliseconds, const string &name){
+	return belle_sip_main_loop_create_cpp_timeout_2(getPrivate()->getMainLoop(), something, (unsigned)milliseconds, name.c_str());
 }
+
 /* Stop and destroy a timer created by createTimer()*/
 void Core::destroyTimer(belle_sip_source_t *timer){
 	belle_sip_main_loop_remove_source(getPrivate()->getMainLoop(), timer);
diff --git a/src/core/core.h b/src/core/core.h
index 4274e305f7d88d8c202dddf02bec65f7bf16e966..9ec89dd8165d072852ff5e946fee7c52c425690c 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -192,10 +192,12 @@ public:
 
 	/*
 	 * Run supplied std::function as a timer. It should return true if repeated, false otherwise.
-	 * It may be unrefed with (with belle_sip_object_unref()) before expiration, if this timer never needs to be cancelled.
+	 * The returned belle_sip_source_t must be unrefed (with belle_sip_object_unref() ).
+	 * It may be unrefed before expiration, if this timer never needs to be cancelled.
 	 */
-	belle_sip_source_t *createTimer(const std::function<bool ()> &something, unsigned int milliseconds);
-	/* Stop (ie cancel) and destroy a timer created by createTimer() */
+	belle_sip_source_t *createTimer(const std::function<bool ()> &something, int millisecond, const std::string &name);
+	/* Stop (ie cancel) and destroy a timer created by createTimer()*/
+
 	void destroyTimer(belle_sip_source_t *timer);
 private:
 	Core ();
diff --git a/src/nat/ice-agent.cpp b/src/nat/ice-agent.cpp
deleted file mode 100644
index 9b69310a832304f291d5d2be80ccddd9d012a32c..0000000000000000000000000000000000000000
--- a/src/nat/ice-agent.cpp
+++ /dev/null
@@ -1,768 +0,0 @@
-/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
- *
- * This file is part of Liblinphone.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include "linphone/core.h"
-
-#include "private.h"
-
-#include "conference/session/media-session-p.h"
-#include "core/core.h"
-#include "logger/logger.h"
-
-#include "ice-agent.h"
-
-// =============================================================================
-
-using namespace std;
-
-LINPHONE_BEGIN_NAMESPACE
-
-bool IceAgent::candidatesGathered () const {
-	if (!iceSession)
-		return false;
-	return !!ice_session_candidates_gathered(iceSession);
-}
-
-void IceAgent::checkSession (IceRole role, bool isReinvite) {
-	// Already created.
-	if (iceSession)
-		return;
-
-	LinphoneConfig *config = linphone_core_get_config(mediaSession.getCore()->getCCore());
-	
-	if (lp_config_get_int(config, "net", "force_ice_disablement", 0)){
-		lWarning()<<"ICE is disabled in this version";
-		return;
-	}
-	
-	if (isReinvite && (lp_config_get_int(config, "net", "allow_late_ice", 0) == 0))
-		return;
-
-	iceSession = ice_session_new();
-
-	// For backward compatibility purposes, shall be enabled by default in the future.
-	ice_session_enable_message_integrity_check(
-		iceSession,
-		!!lp_config_get_int(config, "net", "ice_session_enable_message_integrity_check", 1)
-	);
-	if (lp_config_get_int(config, "net", "dont_default_to_stun_candidates", 0)) {
-		IceCandidateType types[ICT_CandidateTypeMax];
-		types[0] = ICT_HostCandidate;
-		types[1] = ICT_RelayedCandidate;
-		types[2] = ICT_CandidateInvalid;
-		ice_session_set_default_candidates_types(iceSession, types);
-	}
-	ice_session_set_role(iceSession, role);
-}
-
-void IceAgent::deleteSession () {
-	if (!iceSession)
-		return;
-
-	ice_session_destroy(iceSession);
-	iceSession = nullptr;
-	mediaSession.getPrivate()->deactivateIce();
-}
-
-void IceAgent::gatheringFinished () {
-	const SalMediaDescription *rmd = mediaSession.getPrivate()->getOp()->getRemoteMediaDescription();
-	if (rmd)
-		clearUnusedIceCandidates(mediaSession.getPrivate()->getLocalDesc(), rmd);
-	if (!iceSession)
-		return;
-
-	ice_session_compute_candidates_foundations(iceSession);
-	ice_session_eliminate_redundant_candidates(iceSession);
-	ice_session_choose_default_candidates(iceSession);
-
-	int pingTime = ice_session_average_gathering_round_trip_time(iceSession);
-	if (pingTime >= 0) {
-		mediaSession.getPrivate()->setPingTime(pingTime);
-	}
-}
-
-int IceAgent::getNbLosingPairs () const {
-	if (!iceSession)
-		return 0;
-	return ice_session_nb_losing_pairs(iceSession);
-}
-
-bool IceAgent::hasCompleted () const {
-	if (!iceSession)
-		return true;
-	return ice_session_state(iceSession) == IS_Completed;
-}
-
-bool IceAgent::hasCompletedCheckList () const {
-	if (!iceSession)
-		return false;
-	switch (ice_session_state(iceSession)) {
-		case IS_Completed:
-		case IS_Failed:
-			return !!ice_session_has_completed_check_list(iceSession);
-		default:
-			return false;
-	}
-}
-
-bool IceAgent::isControlling () const {
-	if (!iceSession)
-		return false;
-	return ice_session_role(iceSession) == IR_Controlling;
-}
-
-bool IceAgent::prepare (const SalMediaDescription *localDesc, bool incomingOffer, bool allowGathering) {
-	if (!iceSession)
-		return false;
-
-	SalMediaDescription *remoteDesc = nullptr;
-	bool hasVideo = false;
-	if (incomingOffer) {
-		remoteDesc = mediaSession.getPrivate()->getOp()->getRemoteMediaDescription();
-		hasVideo = linphone_core_video_enabled(mediaSession.getCore()->getCCore()) &&
-			linphone_core_media_description_contains_video_stream(remoteDesc);
-	} else
-		hasVideo = mediaSession.getMediaParams()->videoEnabled();
-
-	prepareIceForStream(mediaSession.getPrivate()->getMediaStream(LinphoneStreamTypeAudio), true);
-	if (hasVideo)
-		prepareIceForStream(mediaSession.getPrivate()->getMediaStream(LinphoneStreamTypeVideo), true);
-	if (mediaSession.getMediaParams()->realtimeTextEnabled())
-		prepareIceForStream(mediaSession.getPrivate()->getMediaStream(LinphoneStreamTypeText), true);
-
-	// Start ICE gathering.
-	if (incomingOffer){
-		// This may delete the ice session.
-		updateFromRemoteMediaDescription(localDesc, remoteDesc, true);
-	}
-	if (iceSession && allowGathering && !ice_session_candidates_gathered(iceSession)) {
-		mediaSession.getPrivate()->prepareStreamsForIceGathering(hasVideo);
-		int err = gatherIceCandidates();
-		if (err == 0) {
-			// Ice candidates gathering wasn't started, but we can proceed with the call anyway.
-			mediaSession.getPrivate()->stopStreamsForIceGathering();
-			return false;
-		} else if (err == -1) {
-			mediaSession.getPrivate()->stopStreamsForIceGathering();
-			deleteSession();
-			return false;
-		}
-		return true;
-	}
-	return false;
-}
-
-void IceAgent::prepareIceForStream (MediaStream *ms, bool createChecklist) {
-	if (!iceSession)
-		return;
-
-	int streamIndex = mediaSession.getPrivate()->getStreamIndex(ms);
-	rtp_session_set_pktinfo(ms->sessions.rtp_session, true);
-	IceCheckList *cl = ice_session_check_list(iceSession, streamIndex);
-	if (!cl && createChecklist) {
-		cl = ice_check_list_new();
-		ice_session_add_check_list(iceSession, cl, static_cast<unsigned int>(streamIndex));
-		lInfo() << "Created new ICE check list for stream [" << streamIndex << "]";
-	}
-	if (cl)
-		media_stream_set_ice_check_list(ms, cl);
-}
-
-void IceAgent::resetSession (IceRole role) {
-	if (!iceSession)
-		return;
-	ice_session_reset(iceSession, role);
-}
-
-void IceAgent::restartSession (IceRole role) {
-	if (!iceSession)
-		return;
-	ice_session_restart(iceSession, role);
-}
-
-void IceAgent::startConnectivityChecks () {
-	if (!iceSession)
-		return;
-	ice_session_start_connectivity_checks(iceSession);
-}
-
-void IceAgent::stopIceForInactiveStreams (SalMediaDescription *desc) {
-	if (!iceSession)
-		return;
-	if (ice_session_state(iceSession) == IS_Completed)
-		return;
-	for (int i = 0; i < desc->nb_streams; i++) {
-		IceCheckList *cl = ice_session_check_list(iceSession, i);
-		if (!sal_stream_description_active(&desc->streams[i]) && cl) {
-			ice_session_remove_check_list(iceSession, cl);
-			mediaSession.getPrivate()->clearIceCheckList(cl);
-		}
-	}
-	updateIceStateInCallStats();
-}
-
-void IceAgent::updateFromRemoteMediaDescription (
-	const SalMediaDescription *localDesc,
-	const SalMediaDescription *remoteDesc,
-	bool isOffer
-) {
-	if (!iceSession)
-		return;
-
-	if (!iceParamsFoundInRemoteMediaDescription(remoteDesc)) {
-		// Response from remote does not contain mandatory ICE attributes, delete the session.
-		deleteSession();
-		mediaSession.getPrivate()->enableSymmetricRtp(!!linphone_core_symmetric_rtp_enabled(mediaSession.getCore()->getCCore()));
-		return;
-	}
-
-	// Check for ICE restart and set remote credentials.
-	bool iceRestarted = checkForIceRestartAndSetRemoteCredentials(remoteDesc, isOffer);
-
-	// Create ICE check lists if needed and parse ICE attributes.
-	createIceCheckListsAndParseIceAttributes(remoteDesc, iceRestarted);
-	for (int i = 0; i < remoteDesc->nb_streams; i++) {
-		const SalStreamDescription *stream = &remoteDesc->streams[i];
-		IceCheckList *cl = ice_session_check_list(iceSession, i);
-		if (!cl) continue;
-		if (!sal_stream_description_active(stream)) {
-			ice_session_remove_check_list_from_idx(iceSession, static_cast<unsigned int>(i));
-			mediaSession.getPrivate()->clearIceCheckList(cl);
-		}
-	}
-	clearUnusedIceCandidates(localDesc, remoteDesc);
-	ice_session_check_mismatch(iceSession);
-
-	if (ice_session_nb_check_lists(iceSession) == 0) {
-		deleteSession();
-		mediaSession.getPrivate()->enableSymmetricRtp(!!linphone_core_symmetric_rtp_enabled(mediaSession.getCore()->getCCore()));
-	}
-}
-
-void IceAgent::updateIceStateInCallStats () {
-	if (!iceSession)
-		return;
-	IceCheckList *audioCheckList = ice_session_check_list(iceSession, mediaSession.getPrivate()->getStreamIndex(LinphoneStreamTypeAudio));
-	IceCheckList *videoCheckList = ice_session_check_list(iceSession, mediaSession.getPrivate()->getStreamIndex(LinphoneStreamTypeVideo));
-	IceCheckList *textCheckList = ice_session_check_list(iceSession, mediaSession.getPrivate()->getStreamIndex(LinphoneStreamTypeText));
-	if (!audioCheckList && !videoCheckList && !textCheckList)
-		return;
-
-	LinphoneCallStats *audioStats = mediaSession.getPrivate()->getStats(LinphoneStreamTypeAudio);
-	LinphoneCallStats *videoStats = mediaSession.getPrivate()->getStats(LinphoneStreamTypeVideo);
-	LinphoneCallStats *textStats = mediaSession.getPrivate()->getStats(LinphoneStreamTypeText);
-	IceSessionState sessionState = ice_session_state(iceSession);
-	if ((sessionState == IS_Completed) || ((sessionState == IS_Failed) && ice_session_has_completed_check_list(iceSession))) {
-		_linphone_call_stats_set_ice_state(audioStats, LinphoneIceStateNotActivated);
-		if (audioCheckList && mediaSession.getMediaParams()->audioEnabled())
-			updateIceStateInCallStatsForStream(audioStats, audioCheckList);
-
-		_linphone_call_stats_set_ice_state(videoStats, LinphoneIceStateNotActivated);
-		if (videoCheckList && mediaSession.getMediaParams()->videoEnabled())
-			updateIceStateInCallStatsForStream(videoStats, videoCheckList);
-
-		_linphone_call_stats_set_ice_state(textStats, LinphoneIceStateNotActivated);
-		if (textCheckList && mediaSession.getMediaParams()->realtimeTextEnabled())
-			updateIceStateInCallStatsForStream(textStats, textCheckList);
-	} else if (sessionState == IS_Running) {
-		if (audioCheckList && mediaSession.getMediaParams()->audioEnabled())
-			_linphone_call_stats_set_ice_state(audioStats, LinphoneIceStateInProgress);
-		if (videoCheckList && mediaSession.getMediaParams()->videoEnabled())
-			_linphone_call_stats_set_ice_state(videoStats, LinphoneIceStateInProgress);
-		if (textCheckList && mediaSession.getMediaParams()->realtimeTextEnabled())
-			_linphone_call_stats_set_ice_state(textStats, LinphoneIceStateInProgress);
-	} else {
-		if (audioCheckList && mediaSession.getMediaParams()->audioEnabled())
-			_linphone_call_stats_set_ice_state(audioStats, LinphoneIceStateFailed);
-		if (videoCheckList && mediaSession.getMediaParams()->videoEnabled())
-			_linphone_call_stats_set_ice_state(videoStats, LinphoneIceStateFailed);
-		if (textCheckList && mediaSession.getMediaParams()->realtimeTextEnabled())
-			_linphone_call_stats_set_ice_state(textStats, LinphoneIceStateFailed);
-	}
-	lInfo() << "CallSession [" << &mediaSession << "] New ICE state: audio: [" << linphone_ice_state_to_string(linphone_call_stats_get_ice_state(audioStats)) <<
-		"]    video: [" << linphone_ice_state_to_string(linphone_call_stats_get_ice_state(videoStats)) <<
-		"]    text: [" << linphone_ice_state_to_string(linphone_call_stats_get_ice_state(textStats)) << "]";
-}
-
-void IceAgent::updateLocalMediaDescriptionFromIce (SalMediaDescription *desc) {
-	if (!iceSession)
-		return;
-	IceCandidate *rtpCandidate = nullptr;
-	IceCandidate *rtcpCandidate = nullptr;
-	bool result = false;
-	IceSessionState sessionState = ice_session_state(iceSession);
-	if (sessionState == IS_Completed) {
-		IceCheckList *firstCl = nullptr;
-		for (int i = 0; i < desc->nb_streams; i++) {
-			IceCheckList *cl = ice_session_check_list(iceSession, i);
-			if (cl) {
-				firstCl = cl;
-				break;
-			}
-		}
-		if (firstCl)
-			result = !!ice_check_list_selected_valid_local_candidate(firstCl, &rtpCandidate, nullptr);
-		if (result) {
-			strncpy(desc->addr, rtpCandidate->taddr.ip, sizeof(desc->addr));
-		} else {
-			lWarning() << "If ICE has completed successfully, rtp_candidate should be set!";
-			ice_dump_valid_list(firstCl);
-		}
-	}
-
-	strncpy(desc->ice_pwd, ice_session_local_pwd(iceSession), sizeof(desc->ice_pwd));
-	desc->ice_pwd[sizeof(desc->ice_pwd) - 1] = '\0';
-	strncpy(desc->ice_ufrag, ice_session_local_ufrag(iceSession), sizeof(desc->ice_ufrag));
-	desc->ice_ufrag[sizeof(desc->ice_ufrag) - 1] = '\0';
-	
-	for (int i = 0; i < desc->nb_streams; i++) {
-		SalStreamDescription *stream = &desc->streams[i];
-		IceCheckList *cl = ice_session_check_list(iceSession, i);
-		rtpCandidate = rtcpCandidate = nullptr;
-		if (!sal_stream_description_active(stream) || !cl)
-			continue;
-		if (ice_check_list_state(cl) == ICL_Completed) {
-			LinphoneConfig *config = linphone_core_get_config(mediaSession.getCore()->getCCore());
-			// TODO: Remove `ice_uses_nortpproxy` option, let's say in December 2018.
-			bool useNoRtpProxy = !!lp_config_get_int(config, "sip", "ice_uses_nortpproxy", false);
-			if (useNoRtpProxy)
-				stream->set_nortpproxy = true;
-			result = !!ice_check_list_selected_valid_local_candidate(ice_session_check_list(iceSession, i), &rtpCandidate, &rtcpCandidate);
-		} else {
-			stream->set_nortpproxy = false;
-			result = !!ice_check_list_default_local_candidate(ice_session_check_list(iceSession, i), &rtpCandidate, &rtcpCandidate);
-		}
-		if (result) {
-			strncpy(stream->rtp_addr, rtpCandidate->taddr.ip, sizeof(stream->rtp_addr));
-			strncpy(stream->rtcp_addr, rtcpCandidate->taddr.ip, sizeof(stream->rtcp_addr));
-			stream->rtp_port = rtpCandidate->taddr.port;
-			stream->rtcp_port = rtcpCandidate->taddr.port;
-		} else {
-			memset(stream->rtp_addr, 0, sizeof(stream->rtp_addr));
-			memset(stream->rtcp_addr, 0, sizeof(stream->rtcp_addr));
-		}
-		if ((strlen(ice_check_list_local_pwd(cl)) != strlen(desc->ice_pwd)) || (strcmp(ice_check_list_local_pwd(cl), desc->ice_pwd)))
-			strncpy(stream->ice_pwd, ice_check_list_local_pwd(cl), sizeof(stream->ice_pwd));
-		else
-			memset(stream->ice_pwd, 0, sizeof(stream->ice_pwd));
-		if ((strlen(ice_check_list_local_ufrag(cl)) != strlen(desc->ice_ufrag)) || (strcmp(ice_check_list_local_ufrag(cl), desc->ice_ufrag)))
-			strncpy(stream->ice_ufrag, ice_check_list_local_ufrag(cl), sizeof(stream->ice_ufrag));
-		else
-			memset(stream->ice_pwd, 0, sizeof(stream->ice_pwd));
-		stream->ice_mismatch = ice_check_list_is_mismatch(cl);
-		if ((ice_check_list_state(cl) == ICL_Running) || (ice_check_list_state(cl) == ICL_Completed)) {
-			memset(stream->ice_candidates, 0, sizeof(stream->ice_candidates));
-			int nbCandidates = 0;
-			for (int j = 0; j < MIN((int)bctbx_list_size(cl->local_candidates), SAL_MEDIA_DESCRIPTION_MAX_ICE_CANDIDATES); j++) {
-				SalIceCandidate *salCandidate = &stream->ice_candidates[nbCandidates];
-				IceCandidate *iceCandidate = reinterpret_cast<IceCandidate *>(bctbx_list_nth_data(cl->local_candidates, j));
-				const char *defaultAddr = nullptr;
-				int defaultPort = 0;
-				if (iceCandidate->componentID == 1) {
-					defaultAddr = stream->rtp_addr;
-					defaultPort = stream->rtp_port;
-				} else if (iceCandidate->componentID == 2) {
-					defaultAddr = stream->rtcp_addr;
-					defaultPort = stream->rtcp_port;
-				} else
-					continue;
-				if (defaultAddr[0] == '\0')
-					defaultAddr = desc->addr;
-				// Only include the candidates matching the default destination for each component of the stream if the state is Completed as specified in RFC5245 section 9.1.2.2.
-				if (
-					ice_check_list_state(cl) == ICL_Completed &&
-					!((iceCandidate->taddr.port == defaultPort) && (strlen(iceCandidate->taddr.ip) == strlen(defaultAddr)) && (strcmp(iceCandidate->taddr.ip, defaultAddr) == 0))
-				)
-					continue;
-				strncpy(salCandidate->foundation, iceCandidate->foundation, sizeof(salCandidate->foundation));
-				salCandidate->componentID = iceCandidate->componentID;
-				salCandidate->priority = iceCandidate->priority;
-				strncpy(salCandidate->type, ice_candidate_type(iceCandidate), sizeof(salCandidate->type));
-				strncpy(salCandidate->addr, iceCandidate->taddr.ip, sizeof(salCandidate->addr));
-				salCandidate->port = iceCandidate->taddr.port;
-				if (iceCandidate->base && (iceCandidate->base != iceCandidate)) {
-					strncpy(salCandidate->raddr, iceCandidate->base->taddr.ip, sizeof(salCandidate->raddr));
-					salCandidate->rport = iceCandidate->base->taddr.port;
-				}
-				nbCandidates++;
-			}
-		}
-		if ((ice_check_list_state(cl) == ICL_Completed) && (ice_session_role(iceSession) == IR_Controlling)) {
-			memset(stream->ice_remote_candidates, 0, sizeof(stream->ice_remote_candidates));
-			if (ice_check_list_selected_valid_remote_candidate(cl, &rtpCandidate, &rtcpCandidate)) {
-				strncpy(stream->ice_remote_candidates[0].addr, rtpCandidate->taddr.ip, sizeof(stream->ice_remote_candidates[0].addr));
-				stream->ice_remote_candidates[0].port = rtpCandidate->taddr.port;
-				strncpy(stream->ice_remote_candidates[1].addr, rtcpCandidate->taddr.ip, sizeof(stream->ice_remote_candidates[1].addr));
-				stream->ice_remote_candidates[1].port = rtcpCandidate->taddr.port;
-			} else
-				lError() << "ice: Selected valid remote candidates should be present if the check list is in the Completed state";
-		} else {
-			for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_ICE_REMOTE_CANDIDATES; j++) {
-				stream->ice_remote_candidates[j].addr[0] = '\0';
-				stream->ice_remote_candidates[j].port = 0;
-			}
-		}
-	}
-}
-
-// -----------------------------------------------------------------------------
-
-void IceAgent::addLocalIceCandidates (int family, const char *addr, IceCheckList *audioCl, IceCheckList *videoCl, IceCheckList *textCl) {
-	if ((ice_check_list_state(audioCl) != ICL_Completed) && !ice_check_list_candidates_gathered(audioCl)) {
-		int rtpPort = mediaSession.getPrivate()->getRtpPort(LinphoneStreamTypeAudio);
-		int rtcpPort = mediaSession.getPrivate()->getRtcpPort(LinphoneStreamTypeAudio);
-		ice_add_local_candidate(audioCl, "host", family, addr, rtpPort, 1, nullptr);
-		ice_add_local_candidate(audioCl, "host", family, addr, rtcpPort, 2, nullptr);
-		LinphoneCallStats *audioStats = mediaSession.getPrivate()->getStats(LinphoneStreamTypeAudio);
-		_linphone_call_stats_set_ice_state(audioStats, LinphoneIceStateInProgress);
-	}
-	LinphoneCore *core = mediaSession.getCore()->getCCore();
-	if (linphone_core_video_enabled(core) && videoCl && (ice_check_list_state(videoCl) != ICL_Completed) && !ice_check_list_candidates_gathered(videoCl)) {
-		int rtpPort = mediaSession.getPrivate()->getRtpPort(LinphoneStreamTypeVideo);
-		int rtcpPort = mediaSession.getPrivate()->getRtcpPort(LinphoneStreamTypeVideo);
-		ice_add_local_candidate(videoCl, "host", family, addr, rtpPort, 1, nullptr);
-		ice_add_local_candidate(videoCl, "host", family, addr, rtcpPort, 2, nullptr);
-		LinphoneCallStats *videoStats = mediaSession.getPrivate()->getStats(LinphoneStreamTypeVideo);
-		_linphone_call_stats_set_ice_state(videoStats, LinphoneIceStateInProgress);
-	}
-	if (mediaSession.getMediaParams()->realtimeTextEnabled() && textCl && (ice_check_list_state(textCl) != ICL_Completed) && !ice_check_list_candidates_gathered(textCl)) {
-		int rtpPort = mediaSession.getPrivate()->getRtpPort(LinphoneStreamTypeText);
-		int rtcpPort = mediaSession.getPrivate()->getRtcpPort(LinphoneStreamTypeText);
-		ice_add_local_candidate(textCl, "host", family, addr, rtpPort, 1, nullptr);
-		ice_add_local_candidate(textCl, "host", family, addr, rtcpPort, 2, nullptr);
-		LinphoneCallStats *textStats = mediaSession.getPrivate()->getStats(LinphoneStreamTypeText);
-		_linphone_call_stats_set_ice_state(textStats, LinphoneIceStateInProgress);
-	}
-}
-
-bool IceAgent::checkForIceRestartAndSetRemoteCredentials (const SalMediaDescription *md, bool isOffer) {
-	bool iceRestarted = false;
-	string addr = md->addr;
-	if ((addr == "0.0.0.0") || (addr == "::0")) {
-		ice_session_restart(iceSession, isOffer ? IR_Controlled : IR_Controlling);
-		iceRestarted = true;
-	} else {
-		for (int i = 0; i < md->nb_streams; i++) {
-			const SalStreamDescription *stream = &md->streams[i];
-			IceCheckList *cl = ice_session_check_list(iceSession, i);
-			string rtpAddr = stream->rtp_addr;
-			if (cl && (rtpAddr == "0.0.0.0")) {
-				ice_session_restart(iceSession, isOffer ? IR_Controlled : IR_Controlling);
-				iceRestarted = true;
-				break;
-			}
-		}
-	}
-	if (!ice_session_remote_ufrag(iceSession) && !ice_session_remote_pwd(iceSession)) {
-		ice_session_set_remote_credentials(iceSession, md->ice_ufrag, md->ice_pwd);
-	} else if (ice_session_remote_credentials_changed(iceSession, md->ice_ufrag, md->ice_pwd)) {
-		if (!iceRestarted) {
-			ice_session_restart(iceSession, isOffer ? IR_Controlled : IR_Controlling);
-			iceRestarted = true;
-		}
-		ice_session_set_remote_credentials(iceSession, md->ice_ufrag, md->ice_pwd);
-	}
-	for (int i = 0; i < md->nb_streams; i++) {
-		const SalStreamDescription *stream = &md->streams[i];
-		IceCheckList *cl = ice_session_check_list(iceSession, i);
-		if (cl && (stream->ice_pwd[0] != '\0') && (stream->ice_ufrag[0] != '\0')) {
-			if (ice_check_list_remote_credentials_changed(cl, stream->ice_ufrag, stream->ice_pwd)) {
-				if (!iceRestarted && ice_check_list_get_remote_ufrag(cl) && ice_check_list_get_remote_pwd(cl)) {
-					// Restart only if remote ufrag/paswd was already set.
-					ice_session_restart(iceSession, isOffer ? IR_Controlled : IR_Controlling);
-					iceRestarted = true;
-				}
-				ice_check_list_set_remote_credentials(cl, stream->ice_ufrag, stream->ice_pwd);
-			}
-		}
-	}
-	return iceRestarted;
-}
-
-void IceAgent::clearUnusedIceCandidates (const SalMediaDescription *localDesc, const SalMediaDescription *remoteDesc) {
-	if (!localDesc)
-		return;
-	for (int i = 0; i < remoteDesc->nb_streams; i++) {
-		const SalStreamDescription *localStream = &localDesc->streams[i];
-		const SalStreamDescription *stream = &remoteDesc->streams[i];
-		IceCheckList *cl = ice_session_check_list(iceSession, i);
-		if (!cl || !localStream)
-			continue;
-		if (stream->rtcp_mux && localStream->rtcp_mux) {
-			ice_check_list_remove_rtcp_candidates(cl);
-		}
-	}
-}
-
-void IceAgent::createIceCheckListsAndParseIceAttributes (const SalMediaDescription *md, bool iceRestarted) {
-	for (int i = 0; i < md->nb_streams; i++) {
-		const SalStreamDescription *stream = &md->streams[i];
-		IceCheckList *cl = ice_session_check_list(iceSession, i);
-		if (!cl)
-			continue;
-		if (stream->ice_mismatch) {
-			ice_check_list_set_state(cl, ICL_Failed);
-			continue;
-		}
-		if (stream->rtp_port == 0) {
-			ice_session_remove_check_list(iceSession, cl);
-			mediaSession.getPrivate()->clearIceCheckList(cl);
-			continue;
-		}
-		if ((stream->ice_pwd[0] != '\0') && (stream->ice_ufrag[0] != '\0'))
-			ice_check_list_set_remote_credentials(cl, stream->ice_ufrag, stream->ice_pwd);
-		for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_ICE_CANDIDATES; j++) {
-			bool defaultCandidate = false;
-			const SalIceCandidate *candidate = &stream->ice_candidates[j];
-			if (candidate->addr[0] == '\0')
-				break;
-			if ((candidate->componentID == 0) || (candidate->componentID > 2))
-				continue;
-			const char *addr = nullptr;
-			int port = 0;
-			getIceDefaultAddrAndPort(static_cast<uint16_t>(candidate->componentID), md, stream, &addr, &port);
-			if (addr && (candidate->port == port) && (strlen(candidate->addr) == strlen(addr)) && (strcmp(candidate->addr, addr) == 0))
-				defaultCandidate = true;
-			int family = AF_INET;
-			if (strchr(candidate->addr, ':'))
-				family = AF_INET6;
-			ice_add_remote_candidate(
-				cl, candidate->type, family, candidate->addr, candidate->port,
-				static_cast<uint16_t>(candidate->componentID),
-				candidate->priority, candidate->foundation, defaultCandidate
-			);
-		}
-		if (!iceRestarted) {
-			bool losingPairsAdded = false;
-			for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_ICE_REMOTE_CANDIDATES; j++) {
-				const SalIceRemoteCandidate *remoteCandidate = &stream->ice_remote_candidates[j];
-				const char *addr = nullptr;
-				int port = 0;
-				int componentID = j + 1;
-				if (remoteCandidate->addr[0] == '\0') break;
-				getIceDefaultAddrAndPort(static_cast<uint16_t>(componentID), md, stream, &addr, &port);
-
-				// If we receive a re-invite with remote-candidates, supply these pairs to the ice check list.
-				// They might be valid pairs already selected, or losing pairs.
-
-				int remoteFamily = AF_INET;
-				if (strchr(remoteCandidate->addr, ':'))
-					remoteFamily = AF_INET6;
-				int family = AF_INET;
-				if (strchr(addr, ':'))
-					family = AF_INET6;
-				ice_add_losing_pair(cl, static_cast<uint16_t>(j + 1), remoteFamily, remoteCandidate->addr, remoteCandidate->port, family, addr, port);
-				losingPairsAdded = true;
-			}
-			if (losingPairsAdded)
-				ice_check_list_check_completed(cl);
-		}
-	}
-}
-
-/** Return values:
- *  1: STUN gathering is started
- *  0: no STUN gathering is started, but it's ok to proceed with ICE anyway (with local candidates only or because STUN gathering was already done before)
- * -1: no gathering started and something went wrong with local candidates. There is no way to start the ICE session.
- */
-int IceAgent::gatherIceCandidates () {
-	if (!iceSession)
-		return -1;
-	IceCheckList *audioCl = ice_session_check_list(iceSession, mediaSession.getPrivate()->getStreamIndex(LinphoneStreamTypeAudio));
-	IceCheckList *videoCl = ice_session_check_list(iceSession, mediaSession.getPrivate()->getStreamIndex(LinphoneStreamTypeVideo));
-	IceCheckList *textCl = ice_session_check_list(iceSession, mediaSession.getPrivate()->getStreamIndex(LinphoneStreamTypeText));
-	if (!audioCl && !videoCl && !textCl)
-		return -1;
-
-	const struct addrinfo *ai = nullptr;
-	LinphoneNatPolicy *natPolicy = mediaSession.getPrivate()->getNatPolicy();
-	if (natPolicy && linphone_nat_policy_stun_server_activated(natPolicy)) {
-		ai = linphone_nat_policy_get_stun_server_addrinfo(natPolicy);
-		if (ai)
-			ai = getIcePreferredStunServerAddrinfo(ai);
-		else
-			lWarning() << "Failed to resolve STUN server for ICE gathering, continuing without STUN";
-	} else
-		lWarning() << "ICE is used without STUN server";
-	LinphoneCore *core = mediaSession.getCore()->getCCore();
-	ice_session_enable_forced_relay(iceSession, core->forced_ice_relay);
-	ice_session_enable_short_turn_refresh(iceSession, core->short_turn_refresh);
-
-	// Gather local host candidates.
-	char localAddr[LINPHONE_IPADDR_SIZE];
-	if (mediaSession.getPrivate()->getAf() == AF_INET6) {
-		if (linphone_core_get_local_ip_for(AF_INET6, nullptr, localAddr) < 0) {
-			lError() << "Fail to get local IPv6";
-		} else
-			addLocalIceCandidates(AF_INET6, localAddr, audioCl, videoCl, textCl);
-	}
-	if (linphone_core_get_local_ip_for(AF_INET, nullptr, localAddr) < 0) {
-		if (mediaSession.getPrivate()->getAf() != AF_INET6) {
-			lError() << "Fail to get local IPv4";
-			return -1;
-		}
-	} else
-		addLocalIceCandidates(AF_INET, localAddr, audioCl, videoCl, textCl);
-	if (ai && natPolicy && linphone_nat_policy_stun_server_activated(natPolicy)) {
-		string server = linphone_nat_policy_get_stun_server(natPolicy);
-		lInfo() << "ICE: gathering candidates from [" << server << "] using " << (linphone_nat_policy_turn_enabled(natPolicy) ? "TURN" : "STUN");
-		// Gather local srflx candidates.
-		ice_session_enable_turn(iceSession, linphone_nat_policy_turn_enabled(natPolicy));
-		ice_session_set_stun_auth_requested_cb(iceSession, MediaSessionPrivate::stunAuthRequestedCb, mediaSession.getPrivate());
-		return ice_session_gather_candidates(iceSession, ai->ai_addr, (socklen_t)ai->ai_addrlen) ? 1 : 0;
-	} else {
-		lInfo() << "ICE: bypass candidates gathering";
-		ice_session_compute_candidates_foundations(iceSession);
-		ice_session_eliminate_redundant_candidates(iceSession);
-		ice_session_choose_default_candidates(iceSession);
-	}
-	return 0;
-}
-
-void IceAgent::getIceDefaultAddrAndPort (
-	uint16_t componentID,
-	const SalMediaDescription *md,
-	const SalStreamDescription *stream,
-	const char **addr,
-	int *port
-) {
-	if (componentID == 1) {
-		*addr = stream->rtp_addr;
-		*port = stream->rtp_port;
-	} else if (componentID == 2) {
-		*addr = stream->rtcp_addr;
-		*port = stream->rtcp_port;
-	} else
-		return;
-	if ((*addr)[0] == '\0') *addr = md->addr;
-}
-
-/**
- * Choose the preferred IP address to use to contact the STUN server from the list of IP addresses
- * the DNS resolution returned. If a NAT64 address is present, use it, otherwise if an IPv4 address
- * is present, use it, otherwise use an IPv6 address if it is present.
- */
-const struct addrinfo *IceAgent::getIcePreferredStunServerAddrinfo (const struct addrinfo *ai) {
-	// Search for NAT64 addrinfo.
-	const struct addrinfo *it = ai;
-	while (it) {
-		if (it->ai_family == AF_INET6) {
-			struct sockaddr_storage ss;
-			socklen_t sslen = sizeof(ss);
-			memset(&ss, 0, sizeof(ss));
-			bctbx_sockaddr_remove_nat64_mapping(it->ai_addr, (struct sockaddr *)&ss, &sslen);
-			if (ss.ss_family == AF_INET) break;
-		}
-		it = it->ai_next;
-	}
-	const struct addrinfo *preferredAi = it;
-	if (!preferredAi) {
-		// Search for IPv4 addrinfo.
-		it = ai;
-		while (it) {
-			if (it->ai_family == AF_INET)
-				break;
-			if ((it->ai_family == AF_INET6) && (it->ai_flags & AI_V4MAPPED))
-				break;
-			it = it->ai_next;
-		}
-		preferredAi = it;
-	}
-	if (!preferredAi) {
-		// Search for IPv6 addrinfo.
-		it = ai;
-		while (it) {
-			if (it->ai_family == AF_INET6)
-				break;
-			it = it->ai_next;
-		}
-		preferredAi = it;
-	}
-	return preferredAi;
-}
-
-bool IceAgent::iceParamsFoundInRemoteMediaDescription (const SalMediaDescription *md) {
-	if ((md->ice_pwd[0] != '\0') && (md->ice_ufrag[0] != '\0'))
-		return true;
-	bool found = false;
-	for (int i = 0; i < md->nb_streams; i++) {
-		const SalStreamDescription *stream = &md->streams[i];
-		IceCheckList *cl = ice_session_check_list(iceSession, i);
-		if (cl) {
-			if ((stream->ice_pwd[0] != '\0') && (stream->ice_ufrag[0] != '\0'))
-				found = true;
-			else {
-				found = false;
-				break;
-			}
-		}
-	}
-	return found;
-}
-
-void IceAgent::updateIceStateInCallStatsForStream (LinphoneCallStats *stats, IceCheckList *cl) {
-	if (ice_check_list_state(cl) != ICL_Completed) {
-		_linphone_call_stats_set_ice_state(stats, LinphoneIceStateFailed);
-		return;
-	}
-
-	switch (ice_check_list_selected_valid_candidate_type(cl)) {
-		case ICT_HostCandidate:
-			_linphone_call_stats_set_ice_state(stats, LinphoneIceStateHostConnection);
-			break;
-		case ICT_ServerReflexiveCandidate:
-		case ICT_PeerReflexiveCandidate:
-			_linphone_call_stats_set_ice_state(stats, LinphoneIceStateReflexiveConnection);
-			break;
-		case ICT_RelayedCandidate:
-			_linphone_call_stats_set_ice_state(stats, LinphoneIceStateRelayConnection);
-			break;
-		case ICT_CandidateInvalid:
-		case ICT_CandidateTypeMax:
-			// Shall not happen.
-			L_ASSERT(false);
-			break;
-	}
-}
-
-bool IceAgent::checkIceReinviteNeedsDeferedResponse(SalMediaDescription *md) {
-	if (!iceSession || (ice_session_state(iceSession) != IS_Running))
-		return false;
-
-	for (int i = 0; i < md->nb_streams; i++) {
-		SalStreamDescription *stream = &md->streams[i];
-		IceCheckList *cl = ice_session_check_list(iceSession, i);
-		if (!cl)
-			continue;
-
-		if (stream->ice_mismatch)
-			return false;
-		if ((stream->rtp_port == 0) || (ice_check_list_state(cl) != ICL_Running))
-			continue;
-
-		for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_ICE_REMOTE_CANDIDATES; j++) {
-			const SalIceRemoteCandidate *remote_candidate = &stream->ice_remote_candidates[j];
-			if (remote_candidate->addr[0] != '\0')
-				return true;
-		}
-	}
-	return false;
-}
-
-LINPHONE_END_NAMESPACE
diff --git a/src/nat/ice-agent.h b/src/nat/ice-agent.h
deleted file mode 100644
index baa763a2cd424e933340fb5295ec752688f5f0ec..0000000000000000000000000000000000000000
--- a/src/nat/ice-agent.h
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
- *
- * This file is part of Liblinphone.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-#ifndef _L_ICE_AGENT_H_
-#define _L_ICE_AGENT_H_
-
-#include <mediastreamer2/ice.h>
-#include <ortp/event.h>
-
-#include "linphone/utils/general.h"
-
-// =============================================================================
-
-L_DECL_C_STRUCT_PREFIX_LESS(SalMediaDescription);
-L_DECL_C_STRUCT_PREFIX_LESS(SalStreamDescription);
-L_DECL_C_STRUCT(LinphoneCallStats);
-L_DECL_C_STRUCT(LinphoneCore);
-L_DECL_C_STRUCT(MediaStream);
-
-class MediaSession;
-
-LINPHONE_BEGIN_NAMESPACE
-
-class IceAgent {
-public:
-	explicit IceAgent (MediaSession &mediaSession) : mediaSession(mediaSession) {}
-
-	bool candidatesGathered () const;
-	void checkSession (IceRole role, bool isReinvite);
-	void deleteSession ();
-	void gatheringFinished ();
-	int getNbLosingPairs () const;
-	IceSession *getIceSession () const {
-		return iceSession;
-	}
-
-	bool hasCompleted () const;
-	bool hasCompletedCheckList () const;
-	bool hasSession () const {
-		return !!iceSession;
-	}
-
-	bool isControlling () const;
-	bool prepare (const SalMediaDescription *localDesc, bool incomingOffer, bool allowGathering = true);
-	void prepareIceForStream (MediaStream *ms, bool createChecklist);
-	void resetSession (IceRole role);
-	void restartSession (IceRole role);
-	void startConnectivityChecks ();
-	void stopIceForInactiveStreams (SalMediaDescription *desc);
-	void updateFromRemoteMediaDescription (const SalMediaDescription *localDesc, const SalMediaDescription *remoteDesc, bool isOffer);
-	void updateIceStateInCallStats ();
-	void updateLocalMediaDescriptionFromIce (SalMediaDescription *desc);
-	/*
-	 * Checks if an incoming offer with ICE needs a delayed answer, because the ice session hasn't completed yet with
-	 * connecvity checks.
-	 */
-	bool checkIceReinviteNeedsDeferedResponse (SalMediaDescription *md);
-
-private:
-	void addLocalIceCandidates (int family, const char *addr, IceCheckList *audioCl, IceCheckList *videoCl, IceCheckList *textCl);
-	bool checkForIceRestartAndSetRemoteCredentials (const SalMediaDescription *md, bool isOffer);
-	void clearUnusedIceCandidates (const SalMediaDescription *localDesc, const SalMediaDescription *remoteDesc);
-	void createIceCheckListsAndParseIceAttributes (const SalMediaDescription *md, bool iceRestarted);
-	int gatherIceCandidates ();
-	void getIceDefaultAddrAndPort (uint16_t componentID, const SalMediaDescription *md, const SalStreamDescription *stream, const char **addr, int *port);
-	const struct addrinfo *getIcePreferredStunServerAddrinfo (const struct addrinfo *ai);
-	bool iceParamsFoundInRemoteMediaDescription (const SalMediaDescription *md);
-	void updateIceStateInCallStatsForStream (LinphoneCallStats *stats, IceCheckList *cl);
-
-private:
-	MediaSession &mediaSession;
-	IceSession *iceSession = nullptr;
-};
-
-LINPHONE_END_NAMESPACE
-
-#endif // ifndef _L_ICE_AGENT_H_
diff --git a/src/nat/ice-service.cpp b/src/nat/ice-service.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0638788446dc790eea41c6a632ea4cd8f6c001a9
--- /dev/null
+++ b/src/nat/ice-service.cpp
@@ -0,0 +1,719 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "private.h"
+
+#include "ice-service.h"
+#include "conference/session/streams.h"
+#include "conference/session/media-session-p.h"
+#include "utils/if-addrs.h"
+
+
+using namespace::std;
+
+LINPHONE_BEGIN_NAMESPACE
+
+IceService::IceService(StreamsGroup & sg) : mStreamsGroup(sg){
+}
+
+IceService::~IceService(){
+	deleteSession();
+}
+
+bool IceService::isActive() const{
+	return mIceSession != nullptr;
+}
+
+bool IceService::hasCompleted() const{
+	if (!isActive()) return true; // Completed because nothing to do.
+	return ice_session_state(mIceSession) == IS_Completed;
+}
+
+MediaSessionPrivate &IceService::getMediaSessionPrivate() const{
+	return mStreamsGroup.getMediaSessionPrivate();
+}
+
+bool IceService::iceFoundInMediaDescription (const SalMediaDescription *md) {
+	if ((md->ice_pwd[0] != '\0') && (md->ice_ufrag[0] != '\0'))
+		return true;
+	for (int i = 0; i < md->nb_streams; i++) {
+		const SalStreamDescription *stream = &md->streams[i];
+		if ((stream->ice_pwd[0] != '\0') && (stream->ice_ufrag[0] != '\0')){
+			return true;
+		}
+		
+	}
+	return false;
+}
+
+void IceService::checkSession (IceRole role) {
+	LinphoneNatPolicy *natPolicy = getMediaSessionPrivate().getNatPolicy();
+	if (!natPolicy || !linphone_nat_policy_ice_enabled(natPolicy)){
+		return;
+	}
+	
+	// Already created.
+	if (mIceSession)
+		return;
+
+	LinphoneConfig *config = linphone_core_get_config(getCCore());
+	
+	if (lp_config_get_int(config, "net", "force_ice_disablement", 0)){
+		lWarning()<<"ICE is disabled in this version";
+		return;
+	}
+	
+	mIceSession = ice_session_new();
+
+	// For backward compatibility purposes, shall be enabled by default in the future.
+	ice_session_enable_message_integrity_check(
+		mIceSession,
+		!!lp_config_get_int(config, "net", "ice_session_enable_message_integrity_check", 1)
+	);
+	if (lp_config_get_int(config, "net", "dont_default_to_stun_candidates", 0)) {
+		IceCandidateType types[ICT_CandidateTypeMax];
+		types[0] = ICT_HostCandidate;
+		types[1] = ICT_RelayedCandidate;
+		types[2] = ICT_CandidateInvalid;
+		ice_session_set_default_candidates_types(mIceSession, types);
+	}
+	ice_session_set_role(mIceSession, role);
+}
+
+void IceService::fillLocalMediaDescription(OfferAnswerContext & ctx){
+	if (!mIceSession) return;
+
+	if (mGatheringFinished){
+		if (ctx.remoteMediaDescription)
+			clearUnusedIceCandidates(ctx.localMediaDescription, ctx.remoteMediaDescription, ctx.localIsOfferer);
+		
+		ice_session_compute_candidates_foundations(mIceSession);
+		ice_session_eliminate_redundant_candidates(mIceSession);
+		ice_session_choose_default_candidates(mIceSession);
+		mGatheringFinished = false;
+	}
+	updateLocalMediaDescriptionFromIce(ctx.localMediaDescription);
+}
+
+void IceService::createStreams(const OfferAnswerContext &params){
+	checkSession(params.localIsOfferer ? IR_Controlling : IR_Controlled);
+	
+	if (!mIceSession) return;
+	
+	const auto & streams = mStreamsGroup.getStreams();
+	for (auto & stream : streams){
+		size_t index = stream->getIndex();
+		params.scopeStreamToIndex(index);
+		bool streamActive = sal_stream_description_enabled(params.localStreamDescription);
+		
+		if (!params.localIsOfferer){
+			int bundleOwnerIndex = sal_media_description_get_index_of_transport_owner(params.remoteMediaDescription, params.remoteStreamDescription);
+			if (bundleOwnerIndex != -1 && bundleOwnerIndex != (int) index){
+				lInfo() << *stream << " is part of a bundle as secondary stream, ICE not needed.";
+				streamActive = false;
+			}
+		}else{
+			RtpInterface *i = dynamic_cast<RtpInterface*>(stream.get());
+			if (i && !i->isTransportOwner()){
+				lInfo() << *stream << " is currently part of a bundle as secondary stream, ICE not needed.";
+				streamActive = false;
+			}
+		}
+		IceCheckList *cl = ice_session_check_list(mIceSession, (int)index);
+		if (!cl && streamActive) {
+			cl = ice_check_list_new();
+			ice_session_add_check_list(mIceSession, cl, static_cast<unsigned int>(index));
+			lInfo() << "Created new ICE check list " << cl << " for stream #" << index;
+		} else if (cl && !streamActive){
+			ice_session_remove_check_list_from_idx(mIceSession, static_cast<unsigned int>(index));
+			cl = nullptr;
+		}
+		stream->setIceCheckList(cl);
+		stream->iceStateChanged();
+	}
+	
+	if (!params.localIsOfferer){
+		if (params.remoteMediaDescription){
+			// This may delete the ice session.
+			updateFromRemoteMediaDescription(params.localMediaDescription, params.remoteMediaDescription, true);
+		}
+	}
+}
+
+bool IceService::prepare(){
+	if (!mIceSession) return false;
+	
+	// Start ICE gathering if needed.
+	if (!ice_session_candidates_gathered(mIceSession)) {
+		int err = gatherIceCandidates();
+		if (err == 0) {
+			// Ice candidates gathering wasn't started, but we can proceed with the call anyway.
+			return false;
+		} else if (err == -1) {
+			deleteSession();
+			return false;
+		}
+		return true;
+	}
+	return false;
+}
+
+LinphoneCore *IceService::getCCore()const{
+	return mStreamsGroup.getCCore();
+}
+
+void IceService::gatherLocalCandidates(){
+	list<string> localAddrs = IfAddrs::fetchLocalAddresses();
+	bool ipv6Allowed = linphone_core_ipv6_enabled(getCCore());
+	
+	const auto & streams = mStreamsGroup.getStreams();
+	for (auto & stream : streams){
+		size_t index = stream->getIndex();
+		IceCheckList *cl = ice_session_check_list(mIceSession, (int)index);
+		if (cl) {
+			if ((ice_check_list_state(cl) != ICL_Completed) && !ice_check_list_candidates_gathered(cl)) {
+				for (const string & addr : localAddrs){
+					int family = addr.find(':') != string::npos ? AF_INET6 : AF_INET;
+					if (family == AF_INET6 && !ipv6Allowed) continue;
+					ice_add_local_candidate(cl, "host", family, addr.c_str(), stream->getPortConfig().rtpPort, 1, nullptr);
+					ice_add_local_candidate(cl, "host", family, addr.c_str(), stream->getPortConfig().rtcpPort, 2, nullptr);
+				}
+			}
+		}
+	}
+}
+
+/** Return values:
+ *  1: STUN gathering is started
+ *  0: no STUN gathering is started, but it's ok to proceed with ICE anyway (with local candidates only or because STUN gathering was already done before)
+ * -1: no gathering started and something went wrong with local candidates. There is no way to start the ICE session.
+ */
+int IceService::gatherIceCandidates () {
+	const struct addrinfo *ai = nullptr;
+	int err = 0;
+	
+	LinphoneNatPolicy *natPolicy = getMediaSessionPrivate().getNatPolicy();
+	if (natPolicy && linphone_nat_policy_stun_server_activated(natPolicy)) {
+		ai = linphone_nat_policy_get_stun_server_addrinfo(natPolicy);
+		if (ai)
+			ai = getIcePreferredStunServerAddrinfo(ai);
+		else
+			lWarning() << "Failed to resolve STUN server for ICE gathering, continuing without STUN";
+	} else
+		lWarning() << "ICE is used without STUN server";
+	LinphoneCore *core = getCCore();
+	ice_session_enable_forced_relay(mIceSession, core->forced_ice_relay);
+	ice_session_enable_short_turn_refresh(mIceSession, core->short_turn_refresh);
+
+	// Gather local host candidates.
+	gatherLocalCandidates();
+	
+	if (ai && natPolicy && linphone_nat_policy_stun_server_activated(natPolicy)) {
+		string server = linphone_nat_policy_get_stun_server(natPolicy);
+		lInfo() << "ICE: gathering candidates from [" << server << "] using " << (linphone_nat_policy_turn_enabled(natPolicy) ? "TURN" : "STUN");
+		// Gather local srflx candidates.
+		ice_session_enable_turn(mIceSession, linphone_nat_policy_turn_enabled(natPolicy));
+		ice_session_set_stun_auth_requested_cb(mIceSession, MediaSessionPrivate::stunAuthRequestedCb, &getMediaSessionPrivate());
+		err = ice_session_gather_candidates(mIceSession, ai->ai_addr, (socklen_t)ai->ai_addrlen) ? 1 : 0;
+	} else {
+		lInfo() << "ICE: bypass candidates gathering";
+	}
+	if (err == 0) gatheringFinished();
+	return err;
+}
+
+bool IceService::checkForIceRestartAndSetRemoteCredentials (const SalMediaDescription *md, bool isOffer) {
+	bool iceRestarted = false;
+	string addr = md->addr;
+	if ((addr == "0.0.0.0") || (addr == "::0")) {
+		ice_session_restart(mIceSession, isOffer ? IR_Controlled : IR_Controlling);
+		iceRestarted = true;
+	} else {
+		for (int i = 0; i < md->nb_streams; i++) {
+			const SalStreamDescription *stream = &md->streams[i];
+			IceCheckList *cl = ice_session_check_list(mIceSession, i);
+			string rtpAddr = stream->rtp_addr;
+			if (cl && (rtpAddr == "0.0.0.0")) {
+				ice_session_restart(mIceSession, isOffer ? IR_Controlled : IR_Controlling);
+				iceRestarted = true;
+				break;
+			}
+		}
+	}
+	if (!ice_session_remote_ufrag(mIceSession) && !ice_session_remote_pwd(mIceSession)) {
+		ice_session_set_remote_credentials(mIceSession, md->ice_ufrag, md->ice_pwd);
+	} else if (ice_session_remote_credentials_changed(mIceSession, md->ice_ufrag, md->ice_pwd)) {
+		if (!iceRestarted) {
+			ice_session_restart(mIceSession, isOffer ? IR_Controlled : IR_Controlling);
+			iceRestarted = true;
+		}
+		ice_session_set_remote_credentials(mIceSession, md->ice_ufrag, md->ice_pwd);
+	}
+	for (int i = 0; i < md->nb_streams; i++) {
+		const SalStreamDescription *stream = &md->streams[i];
+		IceCheckList *cl = ice_session_check_list(mIceSession, i);
+		if (cl && (stream->ice_pwd[0] != '\0') && (stream->ice_ufrag[0] != '\0')) {
+			if (ice_check_list_remote_credentials_changed(cl, stream->ice_ufrag, stream->ice_pwd)) {
+				if (!iceRestarted && ice_check_list_get_remote_ufrag(cl) && ice_check_list_get_remote_pwd(cl)) {
+					// Restart only if remote ufrag/paswd was already set.
+					ice_session_restart(mIceSession, isOffer ? IR_Controlled : IR_Controlling);
+					iceRestarted = true;
+				}
+				ice_check_list_set_remote_credentials(cl, stream->ice_ufrag, stream->ice_pwd);
+			}
+		}
+	}
+	return iceRestarted;
+}
+
+void IceService::getIceDefaultAddrAndPort (
+	uint16_t componentID,
+	const SalMediaDescription *md,
+	const SalStreamDescription *stream,
+	const char **addr,
+	int *port
+) {
+	if (componentID == 1) {
+		*addr = stream->rtp_addr;
+		*port = stream->rtp_port;
+	} else if (componentID == 2) {
+		*addr = stream->rtcp_addr;
+		*port = stream->rtcp_port;
+	} else
+		return;
+	if ((*addr)[0] == '\0') *addr = md->addr;
+}
+
+void IceService::createIceCheckListsAndParseIceAttributes (const SalMediaDescription *md, bool iceRestarted) {
+	for (int i = 0; i < md->nb_streams; i++) {
+		const SalStreamDescription *stream = &md->streams[i];
+		IceCheckList *cl = ice_session_check_list(mIceSession, i);
+		if (!cl)
+			continue;
+		if (stream->ice_mismatch) {
+			ice_check_list_set_state(cl, ICL_Failed);
+			continue;
+		}
+		if (stream->rtp_port == 0) {
+			ice_session_remove_check_list(mIceSession, cl);
+			mStreamsGroup.getStream((size_t)i)->setIceCheckList(nullptr);
+			continue;
+		}
+		if ((stream->ice_pwd[0] != '\0') && (stream->ice_ufrag[0] != '\0'))
+			ice_check_list_set_remote_credentials(cl, stream->ice_ufrag, stream->ice_pwd);
+		for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_ICE_CANDIDATES; j++) {
+			bool defaultCandidate = false;
+			const SalIceCandidate *candidate = &stream->ice_candidates[j];
+			if (candidate->addr[0] == '\0')
+				break;
+			if ((candidate->componentID == 0) || (candidate->componentID > 2))
+				continue;
+			const char *addr = nullptr;
+			int port = 0;
+			getIceDefaultAddrAndPort(static_cast<uint16_t>(candidate->componentID), md, stream, &addr, &port);
+			if (addr && (candidate->port == port) && (strlen(candidate->addr) == strlen(addr)) && (strcmp(candidate->addr, addr) == 0))
+				defaultCandidate = true;
+			int family = AF_INET;
+			if (strchr(candidate->addr, ':'))
+				family = AF_INET6;
+			ice_add_remote_candidate(
+				cl, candidate->type, family, candidate->addr, candidate->port,
+				static_cast<uint16_t>(candidate->componentID),
+				candidate->priority, candidate->foundation, defaultCandidate
+			);
+		}
+		if (!iceRestarted) {
+			bool losingPairsAdded = false;
+			for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_ICE_REMOTE_CANDIDATES; j++) {
+				const SalIceRemoteCandidate *remoteCandidate = &stream->ice_remote_candidates[j];
+				const char *addr = nullptr;
+				int port = 0;
+				int componentID = j + 1;
+				if (remoteCandidate->addr[0] == '\0') break;
+				getIceDefaultAddrAndPort(static_cast<uint16_t>(componentID), md, stream, &addr, &port);
+
+				// If we receive a re-invite with remote-candidates, supply these pairs to the ice check list.
+				// They might be valid pairs already selected, or losing pairs.
+
+				int remoteFamily = AF_INET;
+				if (strchr(remoteCandidate->addr, ':'))
+					remoteFamily = AF_INET6;
+				int family = AF_INET;
+				if (strchr(addr, ':'))
+					family = AF_INET6;
+				ice_add_losing_pair(cl, static_cast<uint16_t>(j + 1), remoteFamily, remoteCandidate->addr, remoteCandidate->port, family, addr, port);
+				losingPairsAdded = true;
+			}
+			if (losingPairsAdded)
+				ice_check_list_check_completed(cl);
+		}
+	}
+}
+
+void IceService::clearUnusedIceCandidates (const SalMediaDescription *localDesc, const SalMediaDescription *remoteDesc, bool localIsOfferer) {
+	for (int i = 0; i < remoteDesc->nb_streams; i++) {
+		const SalStreamDescription *localStream = &localDesc->streams[i];
+		const SalStreamDescription *stream = &remoteDesc->streams[i];
+		IceCheckList *cl = ice_session_check_list(mIceSession, i);
+		if (!cl || !localStream)
+			continue;
+		if ((localIsOfferer && stream->rtcp_mux && localStream->rtcp_mux)
+			|| (!localIsOfferer && stream->rtcp_mux)) {
+			ice_check_list_remove_rtcp_candidates(cl);
+		}
+	}
+}
+
+void IceService::updateFromRemoteMediaDescription(const SalMediaDescription *localDesc, const SalMediaDescription *remoteDesc, bool isOffer) {
+	if (!mIceSession)
+		return;
+
+	if (!iceFoundInMediaDescription(remoteDesc)) {
+		// Response from remote does not contain mandatory ICE attributes, delete the session.
+		deleteSession();
+		return;
+	}
+
+	// Check for ICE restart and set remote credentials.
+	bool iceRestarted = checkForIceRestartAndSetRemoteCredentials(remoteDesc, isOffer);
+
+	// Create ICE check lists if needed and parse ICE attributes.
+	createIceCheckListsAndParseIceAttributes(remoteDesc, iceRestarted);
+	for (int i = 0; i < remoteDesc->nb_streams; i++) {
+		const SalStreamDescription *stream = &remoteDesc->streams[i];
+		IceCheckList *cl = ice_session_check_list(mIceSession, i);
+		if (!cl) continue;
+		if (!sal_stream_description_enabled(stream) || stream->rtp_port == 0) {
+			/*
+			 * rtp_port == 0 is true when it is a secondary stream part of bundle.
+			 */
+			ice_session_remove_check_list_from_idx(mIceSession, static_cast<unsigned int>(i));
+			auto stream = mStreamsGroup.getStream(i);
+			stream->setIceCheckList(nullptr);
+			stream->iceStateChanged();
+			
+		}
+	}
+	clearUnusedIceCandidates(localDesc, remoteDesc, !isOffer);
+	ice_session_check_mismatch(mIceSession);
+
+	if (ice_session_nb_check_lists(mIceSession) == 0) {
+		deleteSession();
+	}
+}
+
+
+void IceService::updateLocalMediaDescriptionFromIce (SalMediaDescription *desc) {
+	if (!mIceSession)
+		return;
+	IceCandidate *rtpCandidate = nullptr;
+	IceCandidate *rtcpCandidate = nullptr;
+	bool result = false;
+	IceSessionState sessionState = ice_session_state(mIceSession);
+	if (sessionState == IS_Completed) {
+		IceCheckList *firstCl = nullptr;
+		for (int i = 0; i < desc->nb_streams; i++) {
+			IceCheckList *cl = ice_session_check_list(mIceSession, i);
+			if (cl) {
+				firstCl = cl;
+				break;
+			}
+		}
+		if (firstCl)
+			result = !!ice_check_list_selected_valid_local_candidate(firstCl, &rtpCandidate, nullptr);
+		if (result) {
+			strncpy(desc->addr, rtpCandidate->taddr.ip, sizeof(desc->addr));
+		} else {
+			lWarning() << "If ICE has completed successfully, rtp_candidate should be set!";
+			ice_dump_valid_list(firstCl);
+		}
+	}
+
+	strncpy(desc->ice_pwd, ice_session_local_pwd(mIceSession), sizeof(desc->ice_pwd)-1);
+	strncpy(desc->ice_ufrag, ice_session_local_ufrag(mIceSession), sizeof(desc->ice_ufrag)-1);
+	
+	for (int i = 0; i < desc->nb_streams; i++) {
+		SalStreamDescription *stream = &desc->streams[i];
+		IceCheckList *cl = ice_session_check_list(mIceSession, i);
+		rtpCandidate = rtcpCandidate = nullptr;
+		if (!sal_stream_description_enabled(stream) || !cl || stream->rtp_port == 0)
+			continue;
+		if (ice_check_list_state(cl) == ICL_Completed) {
+			result = !!ice_check_list_selected_valid_local_candidate(ice_session_check_list(mIceSession, i), &rtpCandidate, &rtcpCandidate);
+		} else {
+			result = !!ice_check_list_default_local_candidate(ice_session_check_list(mIceSession, i), &rtpCandidate, &rtcpCandidate);
+		}
+		if (result) {
+			strncpy(stream->rtp_addr, rtpCandidate->taddr.ip, sizeof(stream->rtp_addr));
+			strncpy(stream->rtcp_addr, rtcpCandidate->taddr.ip, sizeof(stream->rtcp_addr));
+			stream->rtp_port = rtpCandidate->taddr.port;
+			stream->rtcp_port = rtcpCandidate->taddr.port;
+		} else {
+			memset(stream->rtp_addr, 0, sizeof(stream->rtp_addr));
+			memset(stream->rtcp_addr, 0, sizeof(stream->rtcp_addr));
+		}
+		if ((strlen(ice_check_list_local_pwd(cl)) != strlen(desc->ice_pwd)) || (strcmp(ice_check_list_local_pwd(cl), desc->ice_pwd)))
+			strncpy(stream->ice_pwd, ice_check_list_local_pwd(cl), sizeof(stream->ice_pwd) - 1);
+		else
+			memset(stream->ice_pwd, 0, sizeof(stream->ice_pwd));
+		if ((strlen(ice_check_list_local_ufrag(cl)) != strlen(desc->ice_ufrag)) || (strcmp(ice_check_list_local_ufrag(cl), desc->ice_ufrag)))
+			strncpy(stream->ice_ufrag, ice_check_list_local_ufrag(cl), sizeof(stream->ice_ufrag) -1 );
+		else
+			memset(stream->ice_pwd, 0, sizeof(stream->ice_pwd));
+		stream->ice_mismatch = ice_check_list_is_mismatch(cl);
+		if ((ice_check_list_state(cl) == ICL_Running) || (ice_check_list_state(cl) == ICL_Completed)) {
+			memset(stream->ice_candidates, 0, sizeof(stream->ice_candidates));
+			int nbCandidates = 0;
+			for (int j = 0; j < MIN((int)bctbx_list_size(cl->local_candidates), SAL_MEDIA_DESCRIPTION_MAX_ICE_CANDIDATES); j++) {
+				SalIceCandidate *salCandidate = &stream->ice_candidates[nbCandidates];
+				IceCandidate *iceCandidate = reinterpret_cast<IceCandidate *>(bctbx_list_nth_data(cl->local_candidates, j));
+				const char *defaultAddr = nullptr;
+				int defaultPort = 0;
+				if (iceCandidate->componentID == 1) {
+					defaultAddr = stream->rtp_addr;
+					defaultPort = stream->rtp_port;
+				} else if (iceCandidate->componentID == 2) {
+					defaultAddr = stream->rtcp_addr;
+					defaultPort = stream->rtcp_port;
+				} else
+					continue;
+				if (defaultAddr[0] == '\0')
+					defaultAddr = desc->addr;
+				// Only include the candidates matching the default destination for each component of the stream if the state is Completed as specified in RFC5245 section 9.1.2.2.
+				if (
+					ice_check_list_state(cl) == ICL_Completed &&
+					!((iceCandidate->taddr.port == defaultPort) && (strlen(iceCandidate->taddr.ip) == strlen(defaultAddr)) && (strcmp(iceCandidate->taddr.ip, defaultAddr) == 0))
+				)
+					continue;
+				strncpy(salCandidate->foundation, iceCandidate->foundation, sizeof(salCandidate->foundation));
+				salCandidate->componentID = iceCandidate->componentID;
+				salCandidate->priority = iceCandidate->priority;
+				strncpy(salCandidate->type, ice_candidate_type(iceCandidate), sizeof(salCandidate->type) - 1);
+				strncpy(salCandidate->addr, iceCandidate->taddr.ip, sizeof(salCandidate->addr));
+				salCandidate->port = iceCandidate->taddr.port;
+				if (iceCandidate->base && (iceCandidate->base != iceCandidate)) {
+					strncpy(salCandidate->raddr, iceCandidate->base->taddr.ip, sizeof(salCandidate->raddr));
+					salCandidate->rport = iceCandidate->base->taddr.port;
+				}
+				nbCandidates++;
+			}
+		}
+		if ((ice_check_list_state(cl) == ICL_Completed) && (ice_session_role(mIceSession) == IR_Controlling)) {
+			memset(stream->ice_remote_candidates, 0, sizeof(stream->ice_remote_candidates) -1);
+			if (ice_check_list_selected_valid_remote_candidate(cl, &rtpCandidate, &rtcpCandidate)) {
+				strncpy(stream->ice_remote_candidates[0].addr, rtpCandidate->taddr.ip, sizeof(stream->ice_remote_candidates[0].addr));
+				stream->ice_remote_candidates[0].port = rtpCandidate->taddr.port;
+				if (rtcpCandidate){
+					strncpy(stream->ice_remote_candidates[1].addr, rtcpCandidate->taddr.ip, sizeof(stream->ice_remote_candidates[1].addr));
+					stream->ice_remote_candidates[1].port = rtcpCandidate->taddr.port;
+				}
+			} else
+				lError() << "ice: Selected valid remote candidates should be present if the check list is in the Completed state";
+		} else {
+			for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_ICE_REMOTE_CANDIDATES; j++) {
+				stream->ice_remote_candidates[j].addr[0] = '\0';
+				stream->ice_remote_candidates[j].port = 0;
+			}
+		}
+	}
+}
+
+void IceService::gatheringFinished () {
+	if (!mIceSession)
+		return;
+
+	int pingTime = ice_session_average_gathering_round_trip_time(mIceSession);
+	if (pingTime >= 0) {
+		/* FIXME: is ping time still useful for the MediaSession ? */
+		getMediaSessionPrivate().setPingTime(pingTime);
+	}
+	mGatheringFinished = true;
+}
+
+
+/**
+ * Choose the preferred IP address to use to contact the STUN server from the list of IP addresses
+ * the DNS resolution returned. If a NAT64 address is present, use it, otherwise if an IPv4 address
+ * is present, use it, otherwise use an IPv6 address if it is present.
+ */
+const struct addrinfo *IceService::getIcePreferredStunServerAddrinfo (const struct addrinfo *ai) {
+	// Search for NAT64 addrinfo.
+	const struct addrinfo *it = ai;
+	while (it) {
+		if (it->ai_family == AF_INET6) {
+			struct sockaddr_storage ss;
+			socklen_t sslen = sizeof(ss);
+			memset(&ss, 0, sizeof(ss));
+			bctbx_sockaddr_remove_nat64_mapping(it->ai_addr, (struct sockaddr *)&ss, &sslen);
+			if (ss.ss_family == AF_INET) break;
+		}
+		it = it->ai_next;
+	}
+	const struct addrinfo *preferredAi = it;
+	if (!preferredAi) {
+		// Search for IPv4 addrinfo.
+		it = ai;
+		while (it) {
+			if (it->ai_family == AF_INET)
+				break;
+			if ((it->ai_family == AF_INET6) && (it->ai_flags & AI_V4MAPPED))
+				break;
+			it = it->ai_next;
+		}
+		preferredAi = it;
+	}
+	if (!preferredAi) {
+		// Search for IPv6 addrinfo.
+		it = ai;
+		while (it) {
+			if (it->ai_family == AF_INET6)
+				break;
+			it = it->ai_next;
+		}
+		preferredAi = it;
+	}
+	return preferredAi;
+}
+
+void IceService::finishPrepare(){
+	if (!mIceSession) return;
+	gatheringFinished();
+}
+
+void IceService::render(const OfferAnswerContext & ctx, CallSession::State targetState){
+	if (!mIceSession) return;
+	
+	updateFromRemoteMediaDescription(ctx.localMediaDescription, ctx.remoteMediaDescription, !ctx.localIsOfferer);
+	if (mIceSession && ice_session_state(mIceSession) != IS_Completed)
+		ice_session_start_connectivity_checks(mIceSession);
+}
+
+void IceService::sessionConfirmed(const OfferAnswerContext &ctx){
+}
+
+void IceService::stop(){
+	//Nothing to do. The ice session can survive.
+}
+
+void IceService::finish(){
+	deleteSession();
+}
+
+void IceService::deleteSession () {
+	if (!mIceSession)
+		return;
+	/* clear all check lists */
+	for (auto & stream : mStreamsGroup.getStreams())
+		stream->setIceCheckList(nullptr);
+	ice_session_destroy(mIceSession);
+	mIceSession = nullptr;
+}
+
+void IceService::setListener(IceServiceListener *listener){
+	mListener = listener;
+}
+
+void IceService::restartSession (IceRole role) {
+	if (!mIceSession)
+		return;
+	ice_session_restart(mIceSession, role);
+}
+
+void IceService::resetSession() {
+	if (!mIceSession)
+		return;
+	ice_session_reset(mIceSession, IR_Controlling);
+}
+
+bool IceService::hasCompletedCheckList () const {
+	if (!mIceSession)
+		return false;
+	switch (ice_session_state(mIceSession)) {
+		case IS_Completed:
+		case IS_Failed:
+			return !!ice_session_has_completed_check_list(mIceSession);
+		default:
+			return false;
+	}
+}
+
+void IceService::handleIceEvent(const OrtpEvent *ev){
+	OrtpEventType evt = ortp_event_get_type(ev);
+	const OrtpEventData *evd = ortp_event_get_data(const_cast<OrtpEvent*>(ev));
+	
+	switch (evt){
+		case ORTP_EVENT_ICE_SESSION_PROCESSING_FINISHED:
+			if (hasCompletedCheckList()) {
+				if (mListener) mListener->onIceCompleted(*this);
+			}
+		break;
+		case ORTP_EVENT_ICE_GATHERING_FINISHED:
+			if (!evd->info.ice_processing_successful)
+				lWarning() << "No STUN answer from [" << linphone_nat_policy_get_stun_server(getMediaSessionPrivate().getNatPolicy()) << "], continuing without STUN";
+			mStreamsGroup.finishPrepare();
+			if (mListener) mListener->onGatheringFinished(*this);
+		break;
+		case ORTP_EVENT_ICE_LOSING_PAIRS_COMPLETED:
+			if (mListener) mListener->onLosingPairsCompleted(*this);
+		break;
+		case ORTP_EVENT_ICE_RESTART_NEEDED:
+			if (mListener) mListener->onIceRestartNeeded(*this);
+		break;
+		default:
+			lError() << "IceService::handleIceEvent() is passed with a non-ICE event.";
+		break;
+	}
+	/* Notify all the streams of the ICE state change, so that they can update their stats and so on. */
+	for(auto & stream : mStreamsGroup.getStreams()){
+		stream->iceStateChanged();
+	}
+}
+
+bool IceService::isControlling () const {
+	if (!mIceSession)
+		return false;
+	return ice_session_role(mIceSession) == IR_Controlling;
+}
+
+bool IceService::reinviteNeedsDeferedResponse(SalMediaDescription *remoteMd){
+	if (!mIceSession || (ice_session_state(mIceSession) != IS_Running))
+		return false;
+
+	for (int i = 0; i < remoteMd->nb_streams; i++) {
+		SalStreamDescription *stream = &remoteMd->streams[i];
+		IceCheckList *cl = ice_session_check_list(mIceSession, i);
+		if (!cl)
+			continue;
+
+		if (stream->ice_mismatch)
+			return false;
+		if ((stream->rtp_port == 0) || (ice_check_list_state(cl) != ICL_Running))
+			continue;
+
+		for (int j = 0; j < SAL_MEDIA_DESCRIPTION_MAX_ICE_REMOTE_CANDIDATES; j++) {
+			const SalIceRemoteCandidate *remote_candidate = &stream->ice_remote_candidates[j];
+			if (remote_candidate->addr[0] != '\0')
+				return true;
+		}
+	}
+	return false;
+}
+
+
+LINPHONE_END_NAMESPACE
diff --git a/src/nat/ice-service.h b/src/nat/ice-service.h
new file mode 100644
index 0000000000000000000000000000000000000000..f4db58dbec256a70f09ecee7dcddea5f193651c9
--- /dev/null
+++ b/src/nat/ice-service.h
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ice_service_h
+#define ice_service_h
+
+#include <memory>
+
+#include "conference/session/call-session.h"
+#include "conference/session/media-description-renderer.h"
+
+LINPHONE_BEGIN_NAMESPACE
+
+class StreamsGroup;
+class MediaSessionPrivate;
+class IceServiceListener;
+
+class IceService : public MediaDescriptionRenderer{
+public:
+	IceService(StreamsGroup & sg);
+	virtual ~IceService();
+	
+	bool isActive() const;
+	
+	/* Returns true if ICE has completed succesfully. */
+	bool hasCompleted() const;
+	
+	/* Returns true if ICE has finished with the check lists processing, even if it has failed for some of the check list.*/
+	bool hasCompletedCheckList()const;
+	
+	bool isControlling () const;
+	
+	/* The ICE restart procedure as in RFC */
+	void restartSession(IceRole role);
+	
+	/* Called after a network connectivity change, to restart ICE from the beginning.*/
+	void resetSession();
+	
+	/* Returns true if the incoming offer requires a defered response, due to check-list(s) not yet completed.*/
+	bool reinviteNeedsDeferedResponse(SalMediaDescription *remoteMd);
+	
+	void createStreams(const OfferAnswerContext &params);
+	/**
+	 * Called by the StreamsGroup when the local media description must be filled with ICE parameters.
+	 *
+	 */
+	virtual void fillLocalMediaDescription(OfferAnswerContext & ctx) override;
+	/*
+	 * Prepare to run.
+	 */
+	virtual bool prepare() override;
+	/*
+	 * Prepare stage is finishing.
+	 * Called by the StreamsGroup (who receives mediastreamer2 events) when the ICE gathering is finished.
+	 *
+	 */
+	virtual void finishPrepare() override;
+	/*
+	 * Render the streams according to offer answer context.
+	 */
+	virtual void render(const OfferAnswerContext & ctx, CallSession::State targetState) override;
+	/*
+	 * Called to notify that the session is confirmed (corresponding to SIP ACK).
+	 */
+	virtual void sessionConfirmed(const OfferAnswerContext &ctx) override;
+	/*
+	 * Stop rendering streams.
+	 */
+	virtual void stop() override;
+	/*
+	 * Release engine's resource, pending object destruction.
+	 */
+	virtual void finish() override;
+	
+	/*
+	 * Set the listener to get notified of major ICE events. Used by the MediaSession to perform required signaling operations.
+	 */
+	void setListener(IceServiceListener *listener);
+	
+	/*
+	 * Called by streams (who receive oRTP events) to notify ICE related events to the IceService.
+	 * Ideally the IceService should place its own listener to these ortp events, but well oRTP is C and has to be simple.
+	 */
+	void handleIceEvent(const OrtpEvent *ev);
+	
+	/**
+	 * used by non-regression tests only.
+	 */
+	IceSession *getSession()const{
+		return mIceSession;
+	}
+private:
+	MediaSessionPrivate &getMediaSessionPrivate()const;
+	LinphoneCore *getCCore()const;
+	bool iceFoundInMediaDescription (const SalMediaDescription *md);
+	const struct addrinfo *getIcePreferredStunServerAddrinfo (const struct addrinfo *ai);
+	void updateLocalMediaDescriptionFromIce(SalMediaDescription *desc);
+	void getIceDefaultAddrAndPort(uint16_t componentID, const SalMediaDescription *md, const SalStreamDescription *stream, const char **addr, int *port);
+	void clearUnusedIceCandidates (const SalMediaDescription *localDesc, const SalMediaDescription *remoteDesc, bool localIsOfferer);
+	bool checkForIceRestartAndSetRemoteCredentials (const SalMediaDescription *md, bool isOffer);
+	void createIceCheckListsAndParseIceAttributes (const SalMediaDescription *md, bool iceRestarted);
+	void updateFromRemoteMediaDescription (const SalMediaDescription *localDesc, const SalMediaDescription *remoteDesc, bool isOffer);
+	void gatheringFinished();
+	void deleteSession();
+	void checkSession (IceRole role);
+	int gatherIceCandidates ();
+	void gatherLocalCandidates();
+	StreamsGroup & mStreamsGroup;
+	IceSession * mIceSession = nullptr;
+	IceServiceListener *mListener = nullptr;
+	bool mGatheringFinished = false;
+	
+};
+
+class IceServiceListener{
+public:
+	virtual void onGatheringFinished(IceService &service) = 0;
+	virtual void onIceCompleted(IceService &service) = 0;
+	virtual void onLosingPairsCompleted(IceService &service) = 0;
+	virtual void onIceRestartNeeded(IceService & service) = 0;
+	virtual ~IceServiceListener() = default;
+};
+
+LINPHONE_END_NAMESPACE
+
+#endif
+
diff --git a/src/nat/stun-client.cpp b/src/nat/stun-client.cpp
index 60f5f20be53785a4d22329a43827342743235466..07ed9b257a7178c5d72f6c4cc49e0690775096d0 100644
--- a/src/nat/stun-client.cpp
+++ b/src/nat/stun-client.cpp
@@ -147,7 +147,7 @@ int StunClient::run (int audioPort, int videoPort, int textPort) {
 void StunClient::updateMediaDescription (SalMediaDescription *md) const {
 	if (!stunDiscoveryDone) return;
 	for (int i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
-		if (!sal_stream_description_active(&md->streams[i]))
+		if (!sal_stream_description_enabled(&md->streams[i]))
 			continue;
 		if (md->streams[i].type == SalAudio && audioCandidate.port != 0) {
 			strncpy(md->streams[i].rtp_addr, audioCandidate.address.c_str(), sizeof(md->streams[i].rtp_addr));
diff --git a/src/sal/call-op.cpp b/src/sal/call-op.cpp
index 380f7ed5dab2bbb0226391d367b64f775e389359..08733c3f670c75370303a03972e5ca4e179f599d 100644
--- a/src/sal/call-op.cpp
+++ b/src/sal/call-op.cpp
@@ -488,13 +488,13 @@ void SalCallOp::processResponseCb (void *userCtx, const belle_sip_response_event
 							}
 							// Ref the ack request so that it is not destroyed when the call_ack_being_sent callbacks is called
 							belle_sip_object_ref(ack);
+							belle_sip_message_add_header(BELLE_SIP_MESSAGE(ack), BELLE_SIP_HEADER(op->mRoot->mUserAgentHeader));
+							op->mRoot->mCallbacks.call_accepted(op); // INVITE
 							if (op->mSdpAnswer) {
 								setSdp(BELLE_SIP_MESSAGE(ack), op->mSdpAnswer);
 								belle_sip_object_unref(op->mSdpAnswer);
 								op->mSdpAnswer = nullptr;
 							}
-							belle_sip_message_add_header(BELLE_SIP_MESSAGE(ack), BELLE_SIP_HEADER(op->mRoot->mUserAgentHeader));
-							op->mRoot->mCallbacks.call_accepted(op); // INVITE
 							op->mRoot->mCallbacks.call_ack_being_sent(op, reinterpret_cast<SalCustomHeader *>(ack));
 							belle_sip_dialog_send_ack(op->mDialog, ack);
 							belle_sip_object_unref(ack);
diff --git a/src/utils/if-addrs.cpp b/src/utils/if-addrs.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c04d38683f283725ea932cb5d4c5e22be996f3d2
--- /dev/null
+++ b/src/utils/if-addrs.cpp
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "private.h"
+#include "tester_utils.h"
+#include "c-wrapper/internal/c-tools.h"
+
+#ifdef HAVE_GETIFADDRS
+#include <sys/types.h>
+#include <ifaddrs.h>
+#include <net/if.h>
+#endif
+
+#include "if-addrs.h"
+
+using namespace std;
+
+LINPHONE_BEGIN_NAMESPACE
+
+#ifdef HAVE_GETIFADDRS
+list<string> IfAddrs::fetchWithGetIfAddrs(){
+	list<string> ret;
+	struct ifaddrs *ifap = nullptr;
+	
+	lInfo() << "Fetching current local IP addresses using getifaddrs().";
+	
+	if (getifaddrs(&ifap) == 0){
+		struct ifaddrs *ifaddr;
+		for (ifaddr = ifap; ifaddr != nullptr; ifaddr = ifaddr->ifa_next){
+			if (ifaddr->ifa_flags & IFF_LOOPBACK) continue;
+			if (ifaddr->ifa_flags & IFF_UP){
+				struct sockaddr *saddr = ifaddr->ifa_addr;
+				char addr[INET6_ADDRSTRLEN] = { 0 };
+				if (!saddr){
+					lError() << "NULL sockaddr returned by getifaddrs().";
+					continue;
+				}
+				switch (saddr->sa_family){
+					case AF_INET:
+						if (inet_ntop(AF_INET, &((struct sockaddr_in*)saddr)->sin_addr, addr, sizeof(addr)) != nullptr){
+							ret.push_back(addr);
+						}else{
+							lError() << "inet_ntop() failed with AF_INET: " << strerror(errno);
+						}
+					break;
+					case AF_INET6:
+						if (inet_ntop(AF_INET6, &((struct sockaddr_in6*)saddr)->sin6_addr, addr, sizeof(addr)) != nullptr){
+							ret.push_back(addr);
+						}else{
+							lError() << "inet_ntop() failed with AF_INET6: " << strerror(errno);
+						}
+					break;
+					default:
+						// ignored.
+					break;
+				}
+			}
+		}
+		freeifaddrs(ifap);
+	}else{
+		lError() << "getifaddrs(): " << strerror(errno);
+	}
+	return ret;
+}
+#endif
+
+list<string> IfAddrs::fetchLocalAddresses(){
+	list<string> ret;
+	
+#ifdef HAVE_GETIFADDRS
+	ret = fetchWithGetIfAddrs();
+#endif
+	/*
+	 * FIXME: implement here code for WIN32 that fetches all addresses of all interfaces.
+	 */
+	
+	/*
+	 * Finally if none of the above methods worked, fallback with linphone_core_get_local_ip() that uses the socket/connect/getsockname method
+	 * to get the local ip address that has the route to public internet.
+	 */
+	if (ret.empty()){
+		lInfo() << "Fetching local ip addresses using the connect() method.";
+		char localAddr[LINPHONE_IPADDR_SIZE];
+		
+		if (linphone_core_get_local_ip_for(AF_INET6, nullptr, localAddr) == 0) {
+			ret.push_back(localAddr);
+		}else{
+			lInfo() << "IceService::fetchLocalAddresses(): Fail to get default IPv6";
+		}
+		
+		if (linphone_core_get_local_ip_for(AF_INET, nullptr, localAddr) == 0){
+			ret.push_back(localAddr);
+		}else{
+			lInfo() << "IceService::fetchLocalAddresses(): Fail to get default IPv4";
+		}
+	}
+	return ret;
+}
+
+LINPHONE_END_NAMESPACE
+
+bctbx_list_t *linphone_fetch_local_addresses(void){
+	return LinphonePrivate::Wrapper::getCListFromCppList(LinphonePrivate::IfAddrs::fetchLocalAddresses());
+}
+
diff --git a/src/utils/if-addrs.h b/src/utils/if-addrs.h
new file mode 100644
index 0000000000000000000000000000000000000000..2320b55e0598d99f00f37e724c851c54d26d0fb5
--- /dev/null
+++ b/src/utils/if-addrs.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef linphone_if_addrs_h
+#define linphone_if_addrs_h
+
+#include <list>
+#include <string>
+
+LINPHONE_BEGIN_NAMESPACE
+
+class IfAddrs{
+public:
+	static std::list<std::string> fetchLocalAddresses();
+private:
+	static std::list<std::string> fetchWithGetIfAddrs();
+};
+
+LINPHONE_END_NAMESPACE
+
+#endif
diff --git a/tester/CMakeLists.txt b/tester/CMakeLists.txt
index ec196fda3484c09e1316968b3e5592c430987a81..31699ee9c6776c20b698a67200deb3c498ef92e9 100644
--- a/tester/CMakeLists.txt
+++ b/tester/CMakeLists.txt
@@ -223,6 +223,7 @@ set(SOURCE_FILES_C
 	tunnel_tester.c
 	vcard_tester.c
 	video_tester.c
+	call_with_rtp_bundle.c
 )
 
 set(SOURCE_FILES_CXX
diff --git a/tester/call_ice.c b/tester/call_ice.c
index 51cb776f99f46f1da56fd7c13f3a24df09041373..c48e061ae52df926af1a7b7384c6640ed45538f0 100644
--- a/tester/call_ice.c
+++ b/tester/call_ice.c
@@ -223,9 +223,12 @@ static void call_with_ice_and_rtcp_mux_without_reinvite(void){
 	_call_with_rtcp_mux(TRUE, TRUE, TRUE,FALSE);
 }
 
-static bool_t is_matching_local_v4_or_v6(const char *ip, const char *localv4, const char *localv6){
+static bool_t is_matching_a_local_address(const char *ip, const bctbx_list_t *addresses){
 	if (strlen(ip)==0) return FALSE;
-	return strcmp(ip, localv4) == 0 || strcmp(ip, localv6) == 0;
+	for (; addresses != NULL; addresses = addresses->next){
+		if (strcmp(ip, (const char*)addresses->data) == 0) return TRUE;
+	}
+	return FALSE;
 }
 
 /*
@@ -237,6 +240,7 @@ static void call_with_ice_with_default_candidate_not_stun(void){
 	char localip[LINPHONE_IPADDR_SIZE]={0};
 	char localip6[LINPHONE_IPADDR_SIZE]={0};
 	bool_t call_ok;
+	bctbx_list_t *local_addresses = linphone_fetch_local_addresses();
 
 	lp_config_set_int(linphone_core_get_config(marie->lc), "net", "dont_default_to_stun_candidates", 1);
 	linphone_core_set_firewall_policy(marie->lc, LinphonePolicyUseIce);
@@ -246,14 +250,15 @@ static void call_with_ice_with_default_candidate_not_stun(void){
 	call_ok = call(marie, pauline);
 	if (call_ok){
 		check_ice(marie, pauline, LinphoneIceStateHostConnection);
-		BC_ASSERT_TRUE(is_matching_local_v4_or_v6(_linphone_call_get_local_desc(linphone_core_get_current_call(marie->lc))->addr, localip, localip6));
-		BC_ASSERT_TRUE(is_matching_local_v4_or_v6(_linphone_call_get_local_desc(linphone_core_get_current_call(marie->lc))->streams[0].rtp_addr, localip, localip6));
-		BC_ASSERT_TRUE(is_matching_local_v4_or_v6(_linphone_call_get_local_desc(linphone_core_get_current_call(marie->lc))->streams[0].rtp_addr, localip, localip6));
-		BC_ASSERT_TRUE(is_matching_local_v4_or_v6(_linphone_call_get_result_desc(linphone_core_get_current_call(pauline->lc))->streams[0].rtp_addr, localip, localip6)
-				|| is_matching_local_v4_or_v6(_linphone_call_get_result_desc(linphone_core_get_current_call(pauline->lc))->addr, localip, localip6)
+		BC_ASSERT_TRUE(is_matching_a_local_address(_linphone_call_get_local_desc(linphone_core_get_current_call(marie->lc))->addr, local_addresses));
+		BC_ASSERT_TRUE(is_matching_a_local_address(_linphone_call_get_local_desc(linphone_core_get_current_call(marie->lc))->streams[0].rtp_addr, local_addresses));
+		BC_ASSERT_TRUE(is_matching_a_local_address(_linphone_call_get_local_desc(linphone_core_get_current_call(marie->lc))->streams[0].rtp_addr, local_addresses));
+		BC_ASSERT_TRUE(is_matching_a_local_address(_linphone_call_get_result_desc(linphone_core_get_current_call(pauline->lc))->streams[0].rtp_addr, local_addresses)
+				|| is_matching_a_local_address(_linphone_call_get_result_desc(linphone_core_get_current_call(pauline->lc))->addr, local_addresses)
 		);
 	}
 	end_call(marie, pauline);
+	bctbx_list_free_with_data(local_addresses, bctbx_free);
 	linphone_core_manager_destroy(marie);
 	linphone_core_manager_destroy(pauline);
 }
@@ -330,7 +335,9 @@ static void call_with_ice_ipv6(void) {
 	}
 }
 
-/*ICE is not expected to work in this case, however this should not crash*/
+/*ICE is not expected to work in this case, however this should not crash.
+ Updated 08/01/2020: now ICE works also in the case of an INVITE without SDP.
+ */
 static void call_with_ice_no_sdp(void){
 	LinphoneCoreManager* marie = linphone_core_manager_new("marie_rc");
 	LinphoneCoreManager* pauline = linphone_core_manager_new(transport_supported(LinphoneTransportTls) ? "pauline_rc" : "pauline_tcp_rc");
diff --git a/tester/call_single_tester.c b/tester/call_single_tester.c
index 9ebca886b555eac129d6b25e9dd92cf55fea38e5..2494159ab7f8ceff334c8e7e9ba757ee6d905da7 100644
--- a/tester/call_single_tester.c
+++ b/tester/call_single_tester.c
@@ -2857,7 +2857,7 @@ static void early_media_call_with_update_base(bool_t media_change){
 	BC_ASSERT_EQUAL(linphone_core_get_tone_manager_stats(pauline->lc)->number_of_stopRingtone, ringWithEarlyMedia ? 0 : 1, int, "%d");
 	BC_ASSERT_EQUAL(linphone_core_get_tone_manager_stats(marie->lc)->number_of_stopRingbackTone, 1, int, "%d");
 
-	pauline_params = linphone_call_params_copy(linphone_call_get_current_params(pauline_call));
+	pauline_params = linphone_core_create_call_params(pauline->lc, pauline_call);
 
 	if (media_change) {
 		disable_all_audio_codecs_except_one(marie->lc,"pcma",-1);
@@ -3304,7 +3304,11 @@ void check_media_direction(LinphoneCoreManager* mgr, LinphoneCall *call, bctbx_l
 			}
 			switch (video_dir) {
 			case LinphoneMediaDirectionInactive:
-				BC_ASSERT_LOWER((int)linphone_call_stats_get_upload_bandwidth(stats), 5, int, "%i");
+				if (stats){
+					BC_ASSERT_LOWER((int)linphone_call_stats_get_upload_bandwidth(stats), 5, int, "%i");
+				}else{
+					/* it is expected that there is no stats for an inactive stream.*/
+				}
 				break;
 			case LinphoneMediaDirectionSendOnly:
 				expected_recv_iframe = 0;
@@ -3319,7 +3323,7 @@ void check_media_direction(LinphoneCoreManager* mgr, LinphoneCall *call, bctbx_l
 			default:
 				break;
 			}
-			linphone_call_stats_unref(stats);
+			if (stats) linphone_call_stats_unref(stats);
 			BC_ASSERT_TRUE(wait_for_list(lcs, &mgr->stat.number_of_IframeDecoded,current_recv_iframe + expected_recv_iframe,10000));
 		}
 #endif
diff --git a/tester/call_video_tester.c b/tester/call_video_tester.c
index 442bf400be0ac934bfcbbac7fad17697578641cf..3a27ebdd77e9ed1062cd871b6497ac7c460d321b 100644
--- a/tester/call_video_tester.c
+++ b/tester/call_video_tester.c
@@ -998,7 +998,7 @@ static void _call_with_ice_video(LinphoneVideoPolicy caller_policy, LinphoneVide
 	bool_t video_added_by_caller, bool_t video_added_by_callee, bool_t video_removed_by_caller, bool_t video_removed_by_callee, bool_t video_only) {
 	LinphoneCoreManager *marie = linphone_core_manager_new("marie_rc");
 	LinphoneCoreManager* pauline = linphone_core_manager_new(transport_supported(LinphoneTransportTls) ? "pauline_rc" : "pauline_tcp_rc");
-	unsigned int nb_audio_starts = 1, nb_video_starts = 1;
+	unsigned int nb_audio_starts = 1, nb_video_starts = 0;
 	const LinphoneCallParams *marie_remote_params;
 	const LinphoneCallParams *pauline_current_params;
 
@@ -1018,6 +1018,7 @@ static void _call_with_ice_video(LinphoneVideoPolicy caller_policy, LinphoneVide
 	if (video_only) {
 		linphone_core_enable_payload_type(marie->lc, linphone_core_find_payload_type(marie->lc, "PCMU", 8000, 1), FALSE); /* Disable PCMU */
 		linphone_core_enable_payload_type(marie->lc, linphone_core_find_payload_type(marie->lc, "PCMA", 8000, 1), TRUE); /* Enable PCMA */
+		nb_audio_starts = 0;
 	}
 
 	linphone_core_manager_wait_for_stun_resolution(marie);
@@ -1052,6 +1053,8 @@ static void _call_with_ice_video(LinphoneVideoPolicy caller_policy, LinphoneVide
 	if (pauline_current_params){
 		BC_ASSERT_TRUE(linphone_call_params_video_enabled(pauline_current_params) ==
 			(caller_policy.automatically_initiate && callee_policy.automatically_accept));
+		if (linphone_call_params_video_enabled(pauline_current_params))
+			nb_video_starts++;
 	}
 
 	/* Wait for ICE reINVITEs to complete. */
@@ -1064,6 +1067,7 @@ static void _call_with_ice_video(LinphoneVideoPolicy caller_policy, LinphoneVide
 	}
 	BC_ASSERT_TRUE(check_ice(pauline, marie, LinphoneIceStateHostConnection));
 	BC_ASSERT_TRUE(check_nb_media_starts(AUDIO_START, pauline, marie, nb_audio_starts, nb_audio_starts));
+	BC_ASSERT_TRUE(check_nb_media_starts(VIDEO_START, pauline, marie, nb_video_starts, nb_video_starts));
 
 
 	if (caller_policy.automatically_initiate && callee_policy.automatically_accept && (video_added_by_caller || video_added_by_callee)){
@@ -1094,7 +1098,6 @@ static void _call_with_ice_video(LinphoneVideoPolicy caller_policy, LinphoneVide
 	}
 	if (video_removed_by_caller || video_removed_by_callee) {
 		BC_ASSERT_TRUE(check_ice(pauline, marie, LinphoneIceStateHostConnection));
-		nb_video_starts++;
 		BC_ASSERT_TRUE(check_nb_media_starts(VIDEO_START, pauline, marie, nb_video_starts, nb_video_starts));
 
 	}
diff --git a/tester/call_with_rtp_bundle.c b/tester/call_with_rtp_bundle.c
new file mode 100644
index 0000000000000000000000000000000000000000..8625491d6a78d587ec85c91ca95821c46b15126a
--- /dev/null
+++ b/tester/call_with_rtp_bundle.c
@@ -0,0 +1,212 @@
+/*
+ * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "linphone/core.h"
+#include "linphone/lpconfig.h"
+#include "liblinphone_tester.h"
+#include "tester_utils.h"
+#include "mediastreamer2/msutils.h"
+#include "belle-sip/sipstack.h"
+#include <bctoolbox/defs.h>
+
+static void check_rtp_bundle(LinphoneCall *call, bool_t should_be_active){
+	const LinphoneCallParams *remote_params = linphone_call_get_remote_params(call);
+	const LinphoneCallParams *current_params = linphone_call_get_current_params(call);
+	if (should_be_active){
+		BC_ASSERT_TRUE(linphone_call_params_rtp_bundle_enabled(remote_params));
+		BC_ASSERT_TRUE(linphone_call_params_rtp_bundle_enabled(current_params));
+	}else{
+		BC_ASSERT_FALSE(linphone_call_params_rtp_bundle_enabled(remote_params));
+		BC_ASSERT_FALSE(linphone_call_params_rtp_bundle_enabled(current_params));
+	}
+}
+
+static bool_t setup_dtls_srtp(LinphoneCoreManager *marie, LinphoneCoreManager *pauline){
+	if (!linphone_core_media_encryption_supported(marie->lc,LinphoneMediaEncryptionDTLS)){
+		BC_FAIL("SRTP-DTLS not supported.");
+		return FALSE;
+	}
+	linphone_core_set_media_encryption(marie->lc, LinphoneMediaEncryptionDTLS);
+	linphone_core_set_media_encryption(pauline->lc, LinphoneMediaEncryptionDTLS);
+	char *path = bc_tester_file("certificates-marie");
+	linphone_core_set_user_certificates_path(marie->lc, path);
+	bc_free(path);
+	path = bc_tester_file("certificates-pauline");
+	linphone_core_set_user_certificates_path(pauline->lc, path);
+	bc_free(path);
+	belle_sip_mkdir(linphone_core_get_user_certificates_path(marie->lc));
+	belle_sip_mkdir(linphone_core_get_user_certificates_path(pauline->lc));
+	return TRUE;
+}
+
+static void _simple_audio_call(bool_t with_dtls_srtp) {
+	LinphoneCoreManager* marie;
+	LinphoneCoreManager* pauline;
+	LinphoneCall *pauline_call, *marie_call;
+	
+	marie = linphone_core_manager_new( "marie_rc");
+	pauline = linphone_core_manager_new(transport_supported(LinphoneTransportTls) ? "pauline_rc" : "pauline_tcp_rc");
+
+	linphone_core_enable_rtp_bundle(marie->lc, TRUE);
+	
+	if (with_dtls_srtp){
+		setup_dtls_srtp(marie, pauline);
+	}
+	
+	BC_ASSERT_TRUE(call(marie,pauline));
+	pauline_call=linphone_core_get_current_call(pauline->lc);
+	marie_call = linphone_core_get_current_call(marie->lc);
+	
+	if (BC_ASSERT_PTR_NOT_NULL(pauline_call))
+		check_rtp_bundle(pauline_call, TRUE);
+		
+	if (BC_ASSERT_PTR_NOT_NULL(marie_call))
+		check_rtp_bundle(marie_call, TRUE);
+
+	liblinphone_tester_check_rtcp(marie,pauline);
+	end_call(marie,pauline);
+	linphone_core_manager_destroy(pauline);
+	linphone_core_manager_destroy(marie);
+}
+
+static void simple_audio_call(void){
+	_simple_audio_call(FALSE);
+}
+
+static void simple_audio_call_with_srtp_dtls(void){
+	_simple_audio_call(TRUE);
+}
+
+typedef struct params{
+	bool_t with_ice;
+	bool_t with_dtls_srtp;
+} params_t;
+
+static void audio_video_call(const params_t *params) {
+	LinphoneCoreManager* marie;
+	LinphoneCoreManager* pauline;
+	LinphoneCall *pauline_call, *marie_call;
+	LinphoneVideoActivationPolicy *vpol = linphone_factory_create_video_activation_policy(linphone_factory_get());
+	
+	marie = linphone_core_manager_new( "marie_rc");
+	pauline = linphone_core_manager_new(transport_supported(LinphoneTransportTls) ? "pauline_rc" : "pauline_tcp_rc");
+
+	linphone_core_enable_rtp_bundle(marie->lc, TRUE);
+	
+	linphone_video_activation_policy_set_automatically_initiate(vpol, TRUE);
+	linphone_video_activation_policy_set_automatically_accept(vpol, TRUE);
+
+	linphone_core_enable_video_capture(marie->lc, TRUE);
+	linphone_core_enable_video_display(marie->lc, TRUE);
+	linphone_core_enable_video_capture(pauline->lc, TRUE);
+	linphone_core_enable_video_display(pauline->lc, TRUE);
+	
+	linphone_core_set_preferred_video_size_by_name(marie->lc, "QVGA");
+	linphone_core_set_preferred_video_size_by_name(pauline->lc, "QVGA");
+
+	linphone_core_set_video_device(marie->lc, "Mire: Mire (synthetic moving picture)");
+	linphone_core_set_video_device(pauline->lc, "Mire: Mire (synthetic moving picture)");
+	
+	if (params->with_ice){
+		/*enable ICE on both ends*/
+		LinphoneNatPolicy *pol;
+		pol = linphone_core_get_nat_policy(marie->lc);
+		linphone_nat_policy_enable_ice(pol, TRUE);
+		linphone_nat_policy_enable_stun(pol, TRUE);
+		linphone_core_set_nat_policy(marie->lc, pol);
+		pol = linphone_core_get_nat_policy(pauline->lc);
+		linphone_nat_policy_enable_ice(pol, TRUE);
+		linphone_nat_policy_enable_stun(pol, TRUE);
+		linphone_core_set_nat_policy(pauline->lc, pol);
+	}
+	
+	if (params->with_dtls_srtp){
+		setup_dtls_srtp(marie, pauline);
+	}
+	
+	linphone_core_set_video_activation_policy(marie->lc, vpol);
+	linphone_core_set_video_activation_policy(pauline->lc, vpol);
+	linphone_video_activation_policy_unref(vpol);
+	
+	if (!BC_ASSERT_TRUE(call(marie,pauline))) goto end;
+	pauline_call=linphone_core_get_current_call(pauline->lc);
+	marie_call = linphone_core_get_current_call(marie->lc);
+	
+	check_rtp_bundle(pauline_call, TRUE);
+	check_rtp_bundle(marie_call, TRUE);
+	
+	BC_ASSERT_TRUE(linphone_call_params_video_enabled(linphone_call_get_current_params(pauline_call)));
+	BC_ASSERT_TRUE(linphone_call_params_video_enabled(linphone_call_get_current_params(marie_call)));
+	
+	if (params->with_ice){
+		BC_ASSERT_TRUE(check_ice(marie, pauline, LinphoneIceStateHostConnection));
+	}
+	
+	liblinphone_tester_check_rtcp(marie,pauline);
+	liblinphone_tester_set_next_video_frame_decoded_cb(pauline_call);
+	BC_ASSERT_TRUE(wait_for(marie->lc,pauline->lc,&pauline->stat.number_of_IframeDecoded,1));
+	liblinphone_tester_set_next_video_frame_decoded_cb(marie_call);
+	BC_ASSERT_TRUE(wait_for(marie->lc,pauline->lc,&marie->stat.number_of_IframeDecoded,1));
+	
+	if (params->with_dtls_srtp){
+		BC_ASSERT_TRUE(linphone_call_params_get_media_encryption(linphone_call_get_current_params(pauline_call)) == LinphoneMediaEncryptionDTLS);
+		BC_ASSERT_TRUE(linphone_call_params_get_media_encryption(linphone_call_get_current_params(marie_call)) == LinphoneMediaEncryptionDTLS);
+	}
+	
+	
+	end_call(marie,pauline);
+	
+end:
+	linphone_core_manager_destroy(pauline);
+	linphone_core_manager_destroy(marie);
+}
+
+static void simple_audio_video_call(void) {
+	params_t params = {0};
+	audio_video_call(&params);
+}
+
+static void audio_video_call_with_ice(void) {
+	params_t params = {0};
+	params.with_ice = TRUE;
+	audio_video_call(&params);
+}
+
+static void audio_video_call_with_ice_and_dtls_srtp(void) {
+	params_t params = {0};
+	params.with_ice = TRUE;
+	params.with_dtls_srtp = TRUE;
+	audio_video_call(&params);
+}
+
+static test_t call_with_rtp_bundle_tests[] = {
+	TEST_NO_TAG("Simple audio call", simple_audio_call),
+	TEST_NO_TAG("Simple audio call with DTLS-SRTP", simple_audio_call_with_srtp_dtls),
+	TEST_NO_TAG("Simple audio-video call", simple_audio_video_call),
+	TEST_NO_TAG("Audio-video call with ICE", audio_video_call_with_ice),
+	TEST_NO_TAG("Audio-video call with ICE and DTLS-SRTP", audio_video_call_with_ice_and_dtls_srtp)
+};
+
+test_suite_t call_with_rtp_bundle_test_suite = {"Call with RTP bundle", NULL, NULL, liblinphone_tester_before_each, liblinphone_tester_after_each,
+								sizeof(call_with_rtp_bundle_tests) / sizeof(call_with_rtp_bundle_tests[0]), call_with_rtp_bundle_tests};
+								
+
diff --git a/tester/certificates/cn/cafile.pem b/tester/certificates/cn/cafile.pem
index 9f031906a56e59a5d7e3e239d3b0a1daa6d2921e..6bc2237bb092328e3005b1a91b69c2ec27840970 100644
--- a/tester/certificates/cn/cafile.pem
+++ b/tester/certificates/cn/cafile.pem
@@ -19,44 +19,44 @@ FUWGJhPnkrnklmBdVB0l7qXYjR5uf766HDkoDxuLhNifow3IYvsS+L2Y6puRQb9w
 HLMDE29mBDl0WyoX3h0yR0EiAO15V9A7I10=
 -----END CERTIFICATE-----
 
-USERTrust RSA Certification Authority (used for *.linphone.org certificates)
-=====================================
+Usertrust (2020) for *.linphone.org
 -----BEGIN CERTIFICATE-----
-MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
+MIIF6TCCA9GgAwIBAgIQBeTcO5Q4qzuFl8umoZhQ4zANBgkqhkiG9w0BAQwFADCB
 iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
 cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
-BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw
-MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV
-BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
-aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy
-dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
-AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B
-3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY
-tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/
-Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2
-VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT
-79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6
-c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT
-Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l
-c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee
-UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE
-Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
-BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G
-A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF
-Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO
-VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3
-ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs
-8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR
-iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze
-Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ
-XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/
-qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB
-VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
-L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
-jjxDah2nGN59PRbxYvnKkKj9
+BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQw
+OTEyMDAwMDAwWhcNMjQwOTExMjM1OTU5WjBfMQswCQYDVQQGEwJGUjEOMAwGA1UE
+CBMFUGFyaXMxDjAMBgNVBAcTBVBhcmlzMQ4wDAYDVQQKEwVHYW5kaTEgMB4GA1UE
+AxMXR2FuZGkgU3RhbmRhcmQgU1NMIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQCUBC2meZV0/9UAPPWu2JSxKXzAjwsLibmCg5duNyj1ohrP0pIL
+m6jTh5RzhBCf3DXLwi2SrCG5yzv8QMHBgyHwv/j2nPqcghDA0I5O5Q1MsJFckLSk
+QFEW2uSEEi0FXKEfFxkkUap66uEHG4aNAXLy59SDIzme4OFMH2sio7QQZrDtgpbX
+bmq08j+1QvzdirWrui0dOnWbMdw+naxb00ENbLAb9Tr1eeohovj0M1JLJC0epJmx
+bUi8uBL+cnB89/sCdfSN3tbawKAyGlLfOGsuRTg/PwSWAP2h9KK71RfWJ3wbWFmV
+XooS/ZyrgT5SKEhRhWvzkbKGPym1bgNi7tYFAgMBAAGjggF1MIIBcTAfBgNVHSME
+GDAWgBRTeb9aqitKz1SA4dibwJ3ysgNmyzAdBgNVHQ4EFgQUs5Cn2MmvTs1hPJ98
+rV1/Qf1pMOowDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD
+VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMCIGA1UdIAQbMBkwDQYLKwYBBAGy
+MQECAhowCAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNl
+cnRydXN0LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNy
+bDB2BggrBgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRy
+dXN0LmNvbS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZ
+aHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAWGf9
+crJq13xhlhl+2UNG0SZ9yFP6ZrBrLafTqlb3OojQO3LJUP33WbKqaPWMcwO7lWUX
+zi8c3ZgTopHJ7qFAbjyY1lzzsiI8Le4bpOHeICQW8owRc5E69vrOJAKHypPstLbI
+FhfFcvwnQPYT/pOmnVHvPCvYd1ebjGU6NSU2t7WKY28HJ5OxYI2A25bUeo8tqxyI
+yW5+1mUfr13KFj8oRtygNeX56eXVlogMT8a3d2dIhCe2H7Bo26y/d7CQuKLJHDJd
+ArolQ4FCR7vY4Y8MDEZf7kYzawMUgtN+zY+vkNaOJH1AQrRqahfGlZfh8jjNp+20
+J0CT33KpuMZmYzc4ZCIwojvxuch7yPspOqsactIGEk72gtQjbz7Dk+XYtsDe3CMW
+1hMwt6CaDixVBgBwAc/qOR2A24j3pSC4W/0xJmmPLQphgzpHphNULB7j7UTKvGof
+KA5R2d4On3XNDgOVyvnFqSot/kGkoUeuDcL5OWYzSlvhhChZbH2UF3bkRYKtcCD9
+0m9jqNf6oDP6N8v3smWe2lBvP+Sn845dWDKXcCMu5/3EFZucJ48y7RetWIExKREa
+m9T8bJUox04FB6b9HbwZ4ui3uRGKLXASUoWNjDNKD/yZkuBjcNqllEdjB+dYxzFf
+BT02Vf6Dsuimrdfp5gJ0iHRc2jTbkNJtUQoj1iM=
 -----END CERTIFICATE-----
 
-AddTrust External Root (used for *.linphone.org certificates generated before 2020)
+
+AddTrust External Root used for *.linphone.org
 ======================
 -----BEGIN CERTIFICATE-----
 MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEUMBIGA1UEChML
@@ -79,3 +79,5 @@ j7DYd7usQWxHYINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
 e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEXc4g/VhsxOBi0cQ+azcgOno4u
 G+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5amnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
 -----END CERTIFICATE-----
+
+
diff --git a/tester/group_chat_tester.c b/tester/group_chat_tester.c
index 6fb58024e48fb01cf6f4cece007a2a2c36ec23ff..d8d683f5f1d7447c0d40e61a4acc3a4d34401fb3 100644
--- a/tester/group_chat_tester.c
+++ b/tester/group_chat_tester.c
@@ -5368,11 +5368,8 @@ static void group_chat_room_join_one_to_one_chat_room_with_a_new_device_not_noti
 	coresList = bctbx_list_concat(coresList, tmpCoresList);
 	linphone_core_manager_start(pauline, TRUE);
 
-	//wait for first notify to be received by pauline
-	wait_for_list(coresList, NULL, 0, 1000);
-
 	// Marie2 gets the one-to-one chat room with Pauline
-	paulineCr = check_has_chat_room_client_side(coresList, pauline, &initialPaulineStats, confAddr, initialSubject, 1, FALSE);
+	paulineCr = check_creation_chat_room_client_side(coresList, pauline, &initialPaulineStats, confAddr, initialSubject, 1, FALSE);
 	LinphoneAddress *marieAddress = linphone_address_new(linphone_core_get_identity(marie2->lc));
 	LinphoneParticipant *marieParticipant =  linphone_chat_room_find_participant(paulineCr, marieAddress);
 	BC_ASSERT_EQUAL(bctbx_list_size(linphone_participant_get_devices (marieParticipant)), 1, int, "%i");
@@ -5385,14 +5382,11 @@ static void group_chat_room_join_one_to_one_chat_room_with_a_new_device_not_noti
 	tmpCoresList = init_core_for_conference(tmpCoresManagerList);
 	bctbx_list_free(tmpCoresManagerList);
 	coresList = bctbx_list_concat(coresList, tmpCoresList);
+	initialPaulineStats = pauline->stat;
 	linphone_core_manager_start(pauline, TRUE);
 
-
-	//wait for first notify to be received by pauline
-	wait_for_list(coresList, NULL, 0, 1000);
-
 	// Marie2 gets the one-to-one chat room with Pauline
-	paulineCr = check_has_chat_room_client_side(coresList, pauline, &initialPaulineStats, confAddr, initialSubject, 1, FALSE);
+	paulineCr = check_creation_chat_room_client_side(coresList, pauline, &initialPaulineStats, confAddr, initialSubject, 1, FALSE);
 	marieParticipant =  linphone_chat_room_find_participant(paulineCr, marieAddress);
 	BC_ASSERT_EQUAL(bctbx_list_size(linphone_participant_get_devices (marieParticipant)), 1, int, "%i");
 	BC_ASSERT_EQUAL(linphone_chat_room_get_history_events_size(paulineCr), initialPaulineEvent, int, "%i");
@@ -5487,7 +5481,12 @@ static void subscribe_test_after_set_chat_database_path(void) {
 	linphone_core_cbs_set_chat_room_state_changed(cbs, core_chat_room_state_changed);
 	configure_core_for_callbacks(pauline, cbs);
 	linphone_core_cbs_unref(cbs);
+	initialPaulineStats = pauline->stat;
 	linphone_core_manager_start(pauline, TRUE);
+	
+	/* Since pauline has unregistered (in linphone_core_manager_reinit(), the conference server will INVITE it again in the chatroom.
+	 * Wait for this event before doing next steps, otherwise the subject changed notification could be masked. */
+	paulineCr = check_creation_chat_room_client_side(coresList, pauline, &initialPaulineStats, confAddr, initialSubject, 2, FALSE);
 
 	LinphoneAddress *paulineAddress = linphone_address_clone(linphone_proxy_config_get_contact(linphone_core_get_default_proxy_config(pauline->lc)));
 	paulineCr = linphone_core_find_chat_room(pauline->lc, confAddr, paulineAddress);
diff --git a/tester/liblinphone_tester.c b/tester/liblinphone_tester.c
index b70b96ea00d70e31a49afc94ad333402881c7b13..654717834778ef4e9a6cdceecc11a002a7ec6ea8 100644
--- a/tester/liblinphone_tester.c
+++ b/tester/liblinphone_tester.c
@@ -368,6 +368,7 @@ void liblinphone_tester_add_suites() {
 	bc_tester_add_suite(&vcard_test_suite);
 #endif
 	bc_tester_add_suite(&utils_test_suite);
+	bc_tester_add_suite(&call_with_rtp_bundle_test_suite);
 }
 
 void liblinphone_tester_init(void(*ftester_printf)(int level, const char *fmt, va_list args)) {
diff --git a/tester/liblinphone_tester.h b/tester/liblinphone_tester.h
index ec0ec34a4595f4d3907c04b7298fe121a9197819..094fd13693176e0c073353c5aa449bceb93f1a8a 100644
--- a/tester/liblinphone_tester.h
+++ b/tester/liblinphone_tester.h
@@ -88,6 +88,7 @@ extern test_suite_t video_test_suite;
 extern test_suite_t call_recovery_test_suite;
 extern test_suite_t call_with_ice_test_suite;
 extern test_suite_t call_secure_test_suite;
+extern test_suite_t call_with_rtp_bundle_test_suite;
 
 #ifdef VCARD_ENABLED
 	extern test_suite_t vcard_test_suite;
diff --git a/tester/message_tester.c b/tester/message_tester.c
index 02f748c4bd98acbcc9a1d72bfe675e59daf739cc..7f30ecd8f8abdd8604966078d0913f01124d7285 100644
--- a/tester/message_tester.c
+++ b/tester/message_tester.c
@@ -795,14 +795,15 @@ static void file_transfer_2_messages_simultaneously(void) {
 				LinphoneChatMessage *recvMsg2 = marie->stat.last_received_chat_message;
 				BC_ASSERT_EQUAL((unsigned int)bctbx_list_size(linphone_core_get_chat_rooms(marie->lc)), 1, unsigned int, "%u");
 				if (bctbx_list_size(linphone_core_get_chat_rooms(marie->lc)) != 1) {
-					char * buf = ms_strdup_printf("Found %d rooms instead of 1: ", bctbx_list_size(linphone_core_get_chat_rooms(marie->lc)));
+					char * buf = ms_strdup_printf("Found %d rooms instead of 1: ", (int)bctbx_list_size(linphone_core_get_chat_rooms(marie->lc)));
 					const bctbx_list_t *it = linphone_core_get_chat_rooms(marie->lc);
 					while (it) {
 						const LinphoneAddress * peer = linphone_chat_room_get_peer_address(it->data);
-						buf = ms_strcat_printf("%s, ", linphone_address_get_username(peer));
+						buf = ms_strcat_printf(buf, "%s, ", linphone_address_get_username(peer));
 						it = it->next;
 					}
 					ms_error("%s", buf);
+					ms_free(buf);
 				}
 
 				cbs = linphone_chat_message_get_callbacks(recvMsg);
diff --git a/tester/tester.c b/tester/tester.c
index 4fd6edee5d71f66bbec9ed62e24834ce5b387b44..b8dbbe47ee1b36e7ce232ff6500317aa129e490b 100644
--- a/tester/tester.c
+++ b/tester/tester.c
@@ -1498,9 +1498,11 @@ void call_stats_updated(LinphoneCore *lc, LinphoneCall *call, const LinphoneCall
 			counters->number_of_rtcp_received_via_mux++;
 		}
 		rtcp_received(counters, _linphone_call_stats_get_received_rtcp(lstats));
+		BC_ASSERT_TRUE(_linphone_call_stats_has_received_rtcp(lstats));
 	}
 	if (updated & LINPHONE_CALL_STATS_SENT_RTCP_UPDATE ) {
 		counters->number_of_rtcp_sent++;
+		BC_ASSERT_TRUE(_linphone_call_stats_has_sent_rtcp(lstats));
 	}
 	if (updated & LINPHONE_CALL_STATS_PERIODICAL_UPDATE ) {
 		const int tab_size = sizeof counters->audio_download_bandwidth / sizeof(int);
@@ -1861,7 +1863,6 @@ bool_t call_with_params2(LinphoneCoreManager* caller_mgr
 	/*wait ice re-invite*/
 	if (linphone_core_get_firewall_policy(caller_mgr->lc) == LinphonePolicyUseIce
 			&& linphone_core_get_firewall_policy(callee_mgr->lc) == LinphonePolicyUseIce
-			&& !linphone_core_sdp_200_ack_enabled(caller_mgr->lc) /*ice does not work with sdp less invite*/
 			&& lp_config_get_int(linphone_core_get_config(callee_mgr->lc), "sip", "update_call_when_ice_completed", TRUE)
 			&& lp_config_get_int(linphone_core_get_config(callee_mgr->lc), "sip", "update_call_when_ice_completed", TRUE)
 			&& linphone_core_get_media_encryption(caller_mgr->lc) != LinphoneMediaEncryptionDTLS /*no ice-reinvite with DTLS*/) {