diff --git a/.gitmodules b/.gitmodules
index e0dcccaa9..48fe67ce6 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -97,3 +97,6 @@
 [submodule "Telegram/lib_webrtc"]
 	path = Telegram/lib_webrtc
 	url = https://github.com/desktop-app/lib_webrtc.git
+[submodule "Telegram/ThirdParty/tgcalls"]
+	path = Telegram/ThirdParty/tgcalls
+	url = https://github.com/TelegramMessenger/tgcalls.git
diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index b2fd96b26..14b902a46 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -35,6 +35,7 @@ include(cmake/lib_ffmpeg.cmake)
 include(cmake/lib_mtproto.cmake)
 include(cmake/lib_scheme.cmake)
 include(cmake/lib_tgvoip.cmake)
+include(cmake/lib_tgcalls.cmake)
 
 set(style_files
     boxes/boxes.style
@@ -102,6 +103,8 @@ endif()
 
 target_link_libraries(Telegram
 PRIVATE
+    tdesktop::lib_tgcalls_legacy
+    tdesktop::lib_tgcalls
     tdesktop::lib_tgvoip
     tdesktop::lib_mtproto
     tdesktop::lib_scheme
@@ -315,11 +318,6 @@ PRIVATE
     calls/calls_box_controller.h
     calls/calls_call.cpp
     calls/calls_call.h
-    calls/calls_controller.cpp
-    calls/calls_controller.h
-    calls/calls_controller_tgvoip.h
-    calls/calls_controller_webrtc.cpp
-    calls/calls_controller_webrtc.h
     calls/calls_emoji_fingerprint.cpp
     calls/calls_emoji_fingerprint.h
     calls/calls_instance.cpp
diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp
index 81e285735..7746ab533 100644
--- a/Telegram/SourceFiles/calls/calls_call.cpp
+++ b/Telegram/SourceFiles/calls/calls_call.cpp
@@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "calls/calls_call.h"
 
 #include "main/main_session.h"
+#include "main/main_account.h"
+#include "main/main_app_config.h"
 #include "apiwrap.h"
 #include "lang/lang_keys.h"
 #include "boxes/confirm_box.h"
@@ -21,11 +23,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/audio/media_audio_track.h"
 #include "base/platform/base_platform_info.h"
 #include "calls/calls_panel.h"
-#include "calls/calls_controller.h"
 #include "data/data_user.h"
 #include "data/data_session.h"
 #include "facades.h"
 
+#include "tgcalls/Instance.h"
+
+namespace tgcalls {
+class InstanceImpl;
+class InstanceImplLegacy;
+void SetLegacyGlobalServerConfig(const std::string &serverConfig);
+} // namespace tgcalls
+
 namespace Calls {
 namespace {
 
@@ -34,20 +43,23 @@ constexpr auto kHangupTimeoutMs = 5000;
 constexpr auto kSha256Size = 32;
 const auto kDefaultVersion = "2.4.4"_q;
 
+const auto RegisterTag = tgcalls::Register<tgcalls::InstanceImpl>();
+const auto RegisterTagLegacy = tgcalls::Register<tgcalls::InstanceImplLegacy>();
+
 void AppendEndpoint(
-		std::vector<TgVoipEndpoint> &list,
+		std::vector<tgcalls::Endpoint> &list,
 		const MTPPhoneConnection &connection) {
 	connection.match([&](const MTPDphoneConnection &data) {
 		if (data.vpeer_tag().v.length() != 16) {
 			return;
 		}
-		auto endpoint = TgVoipEndpoint{
+		auto endpoint = tgcalls::Endpoint{
 			.endpointId = (int64_t)data.vid().v,
-			.host = TgVoipEdpointHost{
+			.host = tgcalls::EndpointHost{
 				.ipv4 = data.vip().v.toStdString(),
 				.ipv6 = data.vipv6().v.toStdString() },
 			.port = (uint16_t)data.vport().v,
-			.type = TgVoipEndpointType::UdpRelay
+			.type = tgcalls::EndpointType::UdpRelay
 		};
 		const auto tag = data.vpeer_tag().v;
 		if (tag.size() >= 16) {
@@ -83,7 +95,48 @@ uint64 ComputeFingerprint(bytes::const_span authKey) {
 }
 
 [[nodiscard]] QVector<MTPstring> CollectVersionsForApi() {
-	return WrapVersions(CollectControllerVersions());
+	return WrapVersions(tgcalls::Meta::Versions() | ranges::action::reverse);
+}
+
+[[nodiscard]] std::vector<tgcalls::RtcServer> CollectRtcServers(
+		not_null<UserData*> user) {
+	using tgcalls::RtcServer;
+	using List = std::vector<std::map<QString, QString>>;
+
+	auto result = std::vector<RtcServer>();
+	const auto list = user->account().appConfig().get<List>(
+		"rtc_servers",
+		List());
+	result.reserve(list.size() * 2);
+	for (const auto &entry : list) {
+		const auto find = [&](const QString &key) {
+			const auto i = entry.find(key);
+			return (i != entry.end()) ? i->second : QString();
+		};
+		const auto host = find(u"host"_q).toStdString();
+		const auto port = find(u"port"_q).toUShort();
+		const auto username = find(u"username"_q).toStdString();
+		const auto password = find(u"password"_q).toStdString();
+		if (host.empty() || !port) {
+			continue;
+		}
+		result.push_back(RtcServer{
+			.host = host,
+			.port = port,
+			.isTurn = false
+		});
+		if (username.empty() || password.empty()) {
+			continue;
+		}
+		result.push_back(RtcServer{
+			.host = host,
+			.port = port,
+			.login = username,
+			.password = password,
+			.isTurn = true,
+		});
+	}
+	return result;
 }
 
 } // namespace
@@ -165,7 +218,7 @@ void Call::startOutgoing() {
 			MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p
 				| MTPDphoneCallProtocol::Flag::f_udp_reflector),
 			MTP_int(kMinLayer),
-			MTP_int(ControllerMaxLayer()),
+			MTP_int(tgcalls::Meta::MaxLayer()),
 			MTP_vector(CollectVersionsForApi()))
 	)).done([=](const MTPphone_PhoneCall &result) {
 		Expects(result.type() == mtpc_phone_phoneCall);
@@ -246,7 +299,7 @@ void Call::actuallyAnswer() {
 			MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p
 				| MTPDphoneCallProtocol::Flag::f_udp_reflector),
 			MTP_int(kMinLayer),
-			MTP_int(ControllerMaxLayer()),
+			MTP_int(tgcalls::Meta::MaxLayer()),
 			MTP_vector(CollectVersionsForApi()))
 	)).done([=](const MTPphone_PhoneCall &result) {
 		Expects(result.type() == mtpc_phone_phoneCall);
@@ -267,8 +320,8 @@ void Call::actuallyAnswer() {
 
 void Call::setMute(bool mute) {
 	_mute = mute;
-	if (_controller) {
-		_controller->setMuteMicrophone(_mute);
+	if (_instance) {
+		_instance->setMuteMicrophone(_mute);
 	}
 	_muteChanged.notify(_mute);
 }
@@ -294,7 +347,7 @@ void Call::redial() {
 	if (_state.current() != State::Busy) {
 		return;
 	}
-	Assert(_controller == nullptr);
+	Assert(_instance == nullptr);
 	_type = Type::Outgoing;
 	setState(State::Requesting);
 	_answerAfterDhConfigReceived = false;
@@ -303,7 +356,7 @@ void Call::redial() {
 }
 
 QString Call::getDebugLog() const {
-	return QString::fromStdString(_controller->getDebugInfo());
+	return QString::fromStdString(_instance->getDebugInfo());
 }
 
 void Call::startWaitingTrack() {
@@ -418,7 +471,7 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
 		}
 		if (_type == Type::Incoming
 			&& _state.current() == State::ExchangingKeys
-			&& !_controller) {
+			&& !_instance) {
 			startConfirmedCall(data);
 		}
 	} return true;
@@ -429,8 +482,8 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
 			return false;
 		}
 		if (data.is_need_debug()) {
-			auto debugLog = _controller
-				? _controller->getDebugInfo()
+			auto debugLog = _instance
+				? _instance->getDebugInfo()
 				: std::string();
 			if (!debugLog.empty()) {
 				user()->session().api().request(MTPphone_SaveCallDebug(
@@ -478,17 +531,23 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
 
 bool Call::handleSignalingData(
 		const MTPDupdatePhoneCallSignalingData &data) {
-	if (data.vphone_call_id().v != _id || !_controller) {
+	if (data.vphone_call_id().v != _id || !_instance) {
 		return false;
 	}
-	return _controller->receiveSignalingData(data.vdata().v);
+	auto prepared = ranges::view::all(
+		data.vdata().v
+	) | ranges::view::transform([](char byte) {
+		return static_cast<uint8_t>(byte);
+	}) | ranges::to_vector;
+	_instance->receiveSignalingData(std::move(prepared));
+	return true;
 }
 
 void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) {
 	Expects(_type == Type::Outgoing);
 
 	if (_state.current() == State::ExchangingKeys
-		|| _controller) {
+		|| _instance) {
 		LOG(("Call Warning: Unexpected confirmAcceptedCall."));
 		return;
 	}
@@ -516,7 +575,7 @@ void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) {
 			MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p
 				| MTPDphoneCallProtocol::Flag::f_udp_reflector),
 			MTP_int(kMinLayer),
-			MTP_int(ControllerMaxLayer()),
+			MTP_int(tgcalls::Meta::MaxLayer()),
 			MTP_vector(CollectVersionsForApi()))
 	)).done([=](const MTPphone_PhoneCall &result) {
 		Expects(result.type() == mtpc_phone_phoneCall);
@@ -568,52 +627,80 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
 	const auto &protocol = call.vprotocol().c_phoneCallProtocol();
 	const auto &serverConfig = _user->session().serverConfig();
 
-	TgVoipConfig config;
-	config.dataSaving = TgVoipDataSaving::Never;
-	config.enableAEC = !Platform::IsMac10_7OrGreater();
-	config.enableNS = true;
-	config.enableAGC = true;
-	config.enableVolumeControl = true;
-	config.initializationTimeout = serverConfig.callConnectTimeoutMs / 1000.;
-	config.receiveTimeout = serverConfig.callPacketTimeoutMs / 1000.;
-	config.enableP2P = call.is_p2p_allowed();
-	config.maxApiLayer = protocol.vmax_layer().v;
+	const auto weak = base::make_weak(this);
+	auto descriptor = tgcalls::Descriptor{
+		.config = tgcalls::Config{
+			.initializationTimeout = serverConfig.callConnectTimeoutMs / 1000.,
+			.receiveTimeout = serverConfig.callPacketTimeoutMs / 1000.,
+			.dataSaving = tgcalls::DataSaving::Never,
+			.enableP2P = call.is_p2p_allowed(),
+			.enableAEC = !Platform::IsMac10_7OrGreater(),
+			.enableNS = true,
+			.enableAGC = true,
+			.enableVolumeControl = true,
+			.maxApiLayer = protocol.vmax_layer().v,
+		},
+		.videoCapture = nullptr,
+		.stateUpdated = [=](tgcalls::State state) {
+			crl::on_main(weak, [=] {
+				handleControllerStateChange(state);
+			});
+		},
+		.videoStateUpdated = [=](bool state) {
+
+		},
+		.signalBarsUpdated = [=](int count) {
+			crl::on_main(weak, [=] {
+				handleControllerBarCountChange(count);
+			});
+		},
+		.remoteVideoIsActiveUpdated = [=](bool active) {
+		},
+		.signalingDataEmitted = [=](const std::vector<uint8_t> &data) {
+			const auto bytes = QByteArray(
+				reinterpret_cast<const char*>(data.data()),
+				data.size());
+			crl::on_main(weak, [=] {
+				sendSignalingData(bytes);
+			});
+		},
+	};
 	if (Logs::DebugEnabled()) {
 		auto callLogFolder = cWorkingDir() + qsl("DebugLogs");
 		auto callLogPath = callLogFolder + qsl("/last_call_log.txt");
 		auto callLogNative = QDir::toNativeSeparators(callLogPath);
 #ifdef Q_OS_WIN
-		config.logPath = callLogNative.toStdWString();
+		descriptor.config.logPath = callLogNative.toStdWString();
 #else // Q_OS_WIN
 		const auto callLogUtf = QFile::encodeName(callLogNative);
-		config.logPath.resize(callLogUtf.size());
+		descriptor.config.logPath.resize(callLogUtf.size());
 		ranges::copy(callLogUtf, config.logPath.begin());
 #endif // Q_OS_WIN
 		QFile(callLogPath).remove();
 		QDir().mkpath(callLogFolder);
 	}
 
-	auto endpoints = std::vector<TgVoipEndpoint>();
 	for (const auto &connection : call.vconnections().v) {
-		AppendEndpoint(endpoints, connection);
+		AppendEndpoint(descriptor.endpoints, connection);
 	}
 
-	auto proxy = TgVoipProxy();
+	descriptor.rtcServers = CollectRtcServers(_user);
+
 	if (Global::UseProxyForCalls()
 		&& (Global::ProxySettings() == MTP::ProxyData::Settings::Enabled)) {
 		const auto &selected = Global::SelectedProxy();
-		if (selected.supportsCalls()) {
+		if (selected.supportsCalls() && !selected.host.isEmpty()) {
 			Assert(selected.type == MTP::ProxyData::Type::Socks5);
-			proxy.host = selected.host.toStdString();
-			proxy.port = selected.port;
-			proxy.login = selected.user.toStdString();
-			proxy.password = selected.password.toStdString();
+			descriptor.proxy = std::make_unique<tgcalls::Proxy>();
+			descriptor.proxy->host = selected.host.toStdString();
+			descriptor.proxy->port = selected.port;
+			descriptor.proxy->login = selected.user.toStdString();
+			descriptor.proxy->password = selected.password.toStdString();
 		}
 	}
 
-	auto encryptionKey = TgVoipEncryptionKey();
-	encryptionKey.isOutgoing = (_type == Type::Outgoing);
-	encryptionKey.value = ranges::view::all(
+	descriptor.encryptionKey.isOutgoing = (_type == Type::Outgoing);
+	descriptor.encryptionKey.value = ranges::view::all(
 		_authKey
 	) | ranges::view::transform([](bytes::type byte) {
 		return static_cast<uint8_t>(byte);
@@ -623,28 +710,26 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
 			const MTPDphoneCallProtocol &data) {
 		return data.vlibrary_versions().v;
 	}).value(0, MTP_bytes(kDefaultVersion)).v;
-	_controller = MakeController(
-		version.toStdString(),
-		config,
-		TgVoipPersistentState(),
-		endpoints,
-		proxy.host.empty() ? nullptr : &proxy,
-		TgVoipNetworkType::Unknown,
-		encryptionKey,
-		[=](QByteArray data) { sendSignalingData(data); },
-		[=](QImage frame) { displayNextFrame(frame); });
 
-	const auto raw = _controller.get();
-	raw->setOnStateUpdated([=](TgVoipState state) {
-		handleControllerStateChange(raw, state);
-	});
-	raw->setOnSignalBarsUpdated([=](int count) {
-		handleControllerBarCountChange(count);
-	});
+	LOG(("Call Info: Creating instance with version '%1', allowP2P: %2"
+		).arg(QString::fromUtf8(version)
+		).arg(Logs::b(descriptor.config.enableP2P)));
+	_instance = tgcalls::Meta::Create(
+		version.toStdString(),
+		std::move(descriptor));
+	if (!_instance) {
+		LOG(("Call Error: Wrong library version: %1."
+			).arg(QString::fromUtf8(version)));
+		finish(FinishType::Failed);
+		return;
+	}
+
+	const auto raw = _instance.get();
 	if (_mute) {
 		raw->setMuteMicrophone(_mute);
 	}
 	const auto &settings = Core::App().settings();
+	//raw->setIncomingVideoOutput(std::make_shared<rtc::VideoSinkInterface<webrtc::VideoFrame>>())
 	raw->setAudioOutputDevice(
 		settings.callOutputDeviceID().toStdString());
 	raw->setAudioInputDevice(
@@ -654,32 +739,27 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
 	raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled());
 }
 
-void Call::handleControllerStateChange(
-		not_null<Controller*> controller,
-		TgVoipState state) {
-	// NB! Can be called from an arbitrary thread!
-	// This can be called from ~VoIPController()!
-
+void Call::handleControllerStateChange(tgcalls::State state) {
 	switch (state) {
-	case TgVoipState::WaitInit: {
+	case tgcalls::State::WaitInit: {
 		DEBUG_LOG(("Call Info: State changed to WaitingInit."));
-		setStateQueued(State::WaitingInit);
+		setState(State::WaitingInit);
 	} break;
 
-	case TgVoipState::WaitInitAck: {
+	case tgcalls::State::WaitInitAck: {
 		DEBUG_LOG(("Call Info: State changed to WaitingInitAck."));
-		setStateQueued(State::WaitingInitAck);
+		setState(State::WaitingInitAck);
 	} break;
 
-	case TgVoipState::Established: {
+	case tgcalls::State::Established: {
 		DEBUG_LOG(("Call Info: State changed to Established."));
-		setStateQueued(State::Established);
+		setState(State::Established);
 	} break;
 
-	case TgVoipState::Failed: {
-		auto error = QString::fromStdString(controller->getLastError());
+	case tgcalls::State::Failed: {
+		auto error = QString::fromStdString(_instance->getLastError());
 		LOG(("Call Info: State changed to Failed, error: %1.").arg(error));
-		setFailedQueued(error);
+		handleControllerError(error);
 	} break;
 
 	default: LOG(("Call Error: Unexpected state in handleStateChange: %1"
@@ -688,12 +768,7 @@ void Call::handleControllerStateChange(
 }
 
 void Call::handleControllerBarCountChange(int count) {
-	// NB! Can be called from an arbitrary thread!
-	// This can be called from ~VoIPController()!
-
-	crl::on_main(this, [=] {
-		setSignalBarCount(count);
-	});
+	setSignalBarCount(count);
 }
 
 void Call::setSignalBarCount(int count) {
@@ -794,28 +869,28 @@ void Call::setState(State state) {
 }
 
 void Call::setCurrentAudioDevice(bool input, std::string deviceID) {
-	if (_controller) {
+	if (_instance) {
 		if (input) {
-			_controller->setAudioInputDevice(deviceID);
+			_instance->setAudioInputDevice(deviceID);
 		} else {
-			_controller->setAudioOutputDevice(deviceID);
+			_instance->setAudioOutputDevice(deviceID);
 		}
 	}
 }
 
 void Call::setAudioVolume(bool input, float level) {
-	if (_controller) {
+	if (_instance) {
 		if (input) {
-			_controller->setInputVolume(level);
+			_instance->setInputVolume(level);
 		} else {
-			_controller->setOutputVolume(level);
+			_instance->setOutputVolume(level);
 		}
 	}
 }
 
 void Call::setAudioDuckingEnabled(bool enabled) {
-	if (_controller) {
-		_controller->setAudioOutputDuckingEnabled(enabled);
+	if (_instance) {
+		_instance->setAudioOutputDuckingEnabled(enabled);
 	}
 }
 
@@ -846,7 +921,7 @@ void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) {
 
 	setState(hangupState);
 	auto duration = getDurationMs() / 1000;
-	auto connectionId = _controller ? _controller->getPreferredRelayId() : 0;
+	auto connectionId = _instance ? _instance->getPreferredRelayId() : 0;
 	_finishByTimeoutTimer.call(kHangupTimeoutMs, [this, finalState] { setState(finalState); });
 	_api.request(MTPphone_DiscardCall(
 		MTP_flags(0),
@@ -902,9 +977,13 @@ void Call::handleControllerError(const QString &error) {
 }
 
 void Call::destroyController() {
-	if (_controller) {
+	if (_instance) {
+		AssertIsDebug();
+		const auto state = _instance->stop();
+		LOG(("CALL_LOG: %1").arg(QString::fromStdString(state.debugLog)));
+
 		DEBUG_LOG(("Call Info: Destroying call controller.."));
-		_controller.reset();
+		_instance.reset();
 		DEBUG_LOG(("Call Info: Call controller destroyed."));
 	}
 	setSignalBarCount(kSignalBarFinished);
@@ -915,7 +994,7 @@ Call::~Call() {
 }
 
 void UpdateConfig(const std::string &data) {
-	TgVoip::setGlobalServerConfig(data);
+	tgcalls::SetLegacyGlobalServerConfig(data);
 }
 
 } // namespace Calls
diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h
index 582f72fa1..eb2eb8328 100644
--- a/Telegram/SourceFiles/calls/calls_call.h
+++ b/Telegram/SourceFiles/calls/calls_call.h
@@ -19,12 +19,13 @@ class Track;
 } // namespace Audio
 } // namespace Media
 
-enum class TgVoipState;
+namespace tgcalls {
+class Instance;
+enum class State;
+} // namespace tgcalls
 
 namespace Calls {
 
-class Controller;
-
 struct DhConfig {
 	int32 version = 0;
 	int32 g = 0;
@@ -153,9 +154,7 @@ private:
 	void displayNextFrame(QImage frame);
 
 	void generateModExpFirst(bytes::const_span randomSeed);
-	void handleControllerStateChange(
-		not_null<Controller*> controller,
-		TgVoipState state);
+	void handleControllerStateChange(tgcalls::State state);
 	void handleControllerBarCountChange(int count);
 	void createAndStartController(const MTPDphoneCall &call);
 
@@ -202,7 +201,7 @@ private:
 	uint64 _accessHash = 0;
 	uint64 _keyFingerprint = 0;
 
-	std::unique_ptr<Controller> _controller;
+	std::unique_ptr<tgcalls::Instance> _instance;
 
 	std::unique_ptr<Media::Audio::Track> _waitingTrack;
 
diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp
index bf6648a9a..9311eb6a0 100644
--- a/Telegram/SourceFiles/main/main_app_config.cpp
+++ b/Telegram/SourceFiles/main/main_app_config.cpp
@@ -117,6 +117,7 @@ std::vector<QString> AppConfig::getStringArray(
 	return getValue(key, [&](const MTPJSONValue &value) {
 		return value.match([&](const MTPDjsonArray &data) {
 			auto result = std::vector<QString>();
+			result.reserve(data.vvalue().v.size());
 			for (const auto &entry : data.vvalue().v) {
 				if (entry.type() != mtpc_jsonString) {
 					return std::move(fallback);
@@ -130,6 +131,36 @@ std::vector<QString> AppConfig::getStringArray(
 	});
 }
 
+std::vector<std::map<QString, QString>> AppConfig::getStringMapArray(
+		const QString &key,
+		std::vector<std::map<QString, QString>> &&fallback) const {
+	return getValue(key, [&](const MTPJSONValue &value) {
+		return value.match([&](const MTPDjsonArray &data) {
+			auto result = std::vector<std::map<QString, QString>>();
+			result.reserve(data.vvalue().v.size());
+			for (const auto &entry : data.vvalue().v) {
+				if (entry.type() != mtpc_jsonObject) {
+					return std::move(fallback);
+				}
+				auto element = std::map<QString, QString>();
+				for (const auto &field : entry.c_jsonObject().vvalue().v) {
+					const auto &data = field.c_jsonObjectValue();
+					if (data.vvalue().type() != mtpc_jsonString) {
+						return std::move(fallback);
+					}
+					element.emplace(
+						qs(data.vkey()),
+						qs(data.vvalue().c_jsonString().vvalue()));
+				}
+				result.push_back(std::move(element));
+			}
+			return result;
+		}, [&](const auto &data) {
+			return std::move(fallback);
+		});
+	});
+}
+
 bool AppConfig::suggestionCurrent(const QString &key) const {
 	return !_dismissedSuggestions.contains(key)
 		&& ranges::contains(
diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h
index 176d73c58..1235b7b40 100644
--- a/Telegram/SourceFiles/main/main_app_config.h
+++ b/Telegram/SourceFiles/main/main_app_config.h
@@ -25,6 +25,8 @@ public:
 			return getString(key, fallback);
 		} else if constexpr (std::is_same_v<Type, std::vector<QString>>) {
 			return getStringArray(key, std::move(fallback));
+		} else if constexpr (std::is_same_v<Type, std::vector<std::map<QString, QString>>>) {
+			return getStringMapArray(key, std::move(fallback));
 		} else if constexpr (std::is_same_v<Type, bool>) {
 			return getBool(key, fallback);
 		}
@@ -60,6 +62,9 @@ private:
 	[[nodiscard]] std::vector<QString> getStringArray(
 		const QString &key,
 		std::vector<QString> &&fallback) const;
+	[[nodiscard]] std::vector<std::map<QString, QString>> getStringMapArray(
+		const QString &key,
+		std::vector<std::map<QString, QString>> &&fallback) const;
 
 	const not_null<Account*> _account;
 	std::optional<MTP::Sender> _api;
diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls
new file mode 160000
index 000000000..ccba25880
--- /dev/null
+++ b/Telegram/ThirdParty/tgcalls
@@ -0,0 +1 @@
+Subproject commit ccba258805478783804cf273ff8c5edffd9f009a
diff --git a/Telegram/cmake/lib_tgcalls.cmake b/Telegram/cmake/lib_tgcalls.cmake
new file mode 100644
index 000000000..3b6fc933d
--- /dev/null
+++ b/Telegram/cmake/lib_tgcalls.cmake
@@ -0,0 +1,102 @@
+# This file is part of Telegram Desktop,
+# the official desktop application for the Telegram messaging service.
+#
+# For license and copyright information please follow this link:
+# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+
+add_library(lib_tgcalls STATIC)
+init_target(lib_tgcalls)
+add_library(tdesktop::lib_tgcalls ALIAS lib_tgcalls)
+
+set(tgcalls_dir ${third_party_loc}/tgcalls)
+set(tgcalls_loc ${tgcalls_dir}/tgcalls)
+
+nice_target_sources(lib_tgcalls ${tgcalls_loc}
+PRIVATE
+    Manager.cpp
+    Manager.h
+    MediaManager.cpp
+    MediaManager.h
+    NetworkManager.cpp
+    NetworkManager.h
+    Instance.cpp
+    Instance.h
+    InstanceImpl.cpp
+    InstanceImpl.h
+    ThreadLocalObject.h
+    VideoCaptureInterface.cpp
+    VideoCaptureInterface.h
+    VideoCaptureInterfaceImpl.cpp
+    VideoCaptureInterfaceImpl.h
+    VideoCapturerInterface.h
+    platform/PlatformInterface.h
+
+    # Windows
+    platform/windows/WindowsInterface.cpp
+    platform/windows/WindowsInterface.h
+    platform/windows/VideoCapturerInterfaceImpl.cpp
+    platform/windows/VideoCapturerInterfaceImpl.h
+
+    # iOS / macOS
+    platform/darwin/DarwinInterface.h
+    platform/darwin/DarwinInterface.mm
+    platform/darwin/TGRTCDefaultVideoDecoderFactory.h
+    platform/darwin/TGRTCDefaultVideoDecoderFactory.mm
+    platform/darwin/TGRTCDefaultVideoEncoderFactory.h
+    platform/darwin/TGRTCDefaultVideoEncoderFactory.mm
+    platform/darwin/TGRTCVideoDecoderH265.h
+    platform/darwin/TGRTCVideoDecoderH265.mm
+    platform/darwin/TGRTCVideoEncoderH265.h
+    platform/darwin/TGRTCVideoEncoderH265.mm
+    platform/darwin/VideoCameraCapturer.h
+    platform/darwin/VideoCameraCapturer.mm
+    platform/darwin/VideoCapturerInterfaceImpl.h
+    platform/darwin/VideoCapturerInterfaceImpl.mm
+    platform/darwin/VideoMetalView.h
+    platform/darwin/VideoMetalView.mm
+
+    # Linux
+
+    # POSIX
+)
+
+if (WIN32)
+    target_compile_definitions(lib_tgcalls
+    PRIVATE
+        TARGET_OS_WIN
+    )
+endif()
+
+target_include_directories(lib_tgcalls
+PUBLIC
+    ${tgcalls_dir}
+PRIVATE
+    ${tgcalls_loc}
+)
+
+target_link_libraries(lib_tgcalls
+PRIVATE
+    desktop-app::external_webrtc
+)
+
+add_library(lib_tgcalls_legacy STATIC)
+init_target(lib_tgcalls_legacy)
+add_library(tdesktop::lib_tgcalls_legacy ALIAS lib_tgcalls_legacy)
+
+nice_target_sources(lib_tgcalls_legacy ${tgcalls_loc}
+PRIVATE
+    legacy/InstanceImplLegacy.cpp
+    legacy/InstanceImplLegacy.h
+)
+
+target_include_directories(lib_tgcalls_legacy
+PRIVATE
+    ${tgcalls_loc}
+)
+
+target_link_libraries(lib_tgcalls_legacy
+PRIVATE
+    tdesktop::lib_tgcalls
+    tdesktop::lib_tgvoip
+    desktop-app::external_openssl
+)
diff --git a/Telegram/cmake/lib_tgvoip.cmake b/Telegram/cmake/lib_tgvoip.cmake
index 50748c324..27e74d07a 100644
--- a/Telegram/cmake/lib_tgvoip.cmake
+++ b/Telegram/cmake/lib_tgvoip.cmake
@@ -48,8 +48,6 @@ else()
         OpusEncoder.cpp
         OpusEncoder.h
         threading.h
-        TgVoip.cpp
-        TgVoip.h
         VoIPController.cpp
         VoIPGroupController.cpp
         VoIPController.h
diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc
index eb5be1840..d3d96130b 160000
--- a/Telegram/lib_webrtc
+++ b/Telegram/lib_webrtc
@@ -1 +1 @@
-Subproject commit eb5be18405d47a226f6a197280176f82b3e903bd
+Subproject commit d3d96130ba245e985c79c9040459b548416efdf8
diff --git a/docs/building-msvc.md b/docs/building-msvc.md
index 4b80dfe90..d7beba696 100644
--- a/docs/building-msvc.md
+++ b/docs/building-msvc.md
@@ -163,6 +163,16 @@ Open **x86 Native Tools Command Prompt for VS 2019.bat**, go to ***BuildPath***
     jom -j4 install
     cd ..
 
+    mkdir webrtc
+    cd webrtc
+    copy ..\patches\webrtc.gclient .gclient
+    git clone https://github.com/open-webrtc-toolkit/owt-deps-webrtc src
+    gclient sync
+    cd src
+    ..\..\..\tdesktop\Telegram\lib_webrtc\gn_build_webrtc.bat
+    ninja -C out/Debug webrtc test:platform_video_capturer test:video_test_common
+    cd ..
+
 ## Build the project
 
 Go to ***BuildPath*\\tdesktop\\Telegram** and run (using [your **api_id** and **api_hash**](#obtain-your-api-credentials))