From 552343fa37fdec7c0be9adcdf05376d02a034465 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 4 Oct 2024 14:38:49 +0400
Subject: [PATCH] PoC video messages sending.

---
 Telegram/SourceFiles/apiwrap.cpp              |   2 +
 Telegram/SourceFiles/apiwrap.h                |   1 +
 .../ui/desktop_capture_choose_source.cpp      |   4 +-
 .../SourceFiles/ffmpeg/ffmpeg_utility.cpp     | 173 ++++-
 Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h  |  47 +-
 .../SourceFiles/history/history_widget.cpp    |   1 +
 .../view/controls/compose_controls_common.h   |   1 +
 .../history_view_voice_record_bar.cpp         |  88 ++-
 .../controls/history_view_voice_record_bar.h  |  11 +-
 .../view/history_view_replies_section.cpp     |   1 +
 .../view/history_view_scheduled_section.cpp   | 288 ++++----
 .../view/history_view_scheduled_section.h     |  13 +-
 .../media/audio/media_audio_capture.cpp       |  75 +-
 .../media/audio/media_audio_capture.h         |  11 +-
 .../media/stories/media_stories_reply.cpp     |   1 +
 .../business/settings_shortcut_messages.cpp   |   1 +
 .../SourceFiles/storage/localimageloader.cpp  |  47 +-
 .../SourceFiles/storage/localimageloader.h    |   2 +
 .../ui/controls/round_video_recorder.cpp      | 649 ++++++++++++++++++
 .../ui/controls/round_video_recorder.h        |  65 ++
 Telegram/cmake/td_ui.cmake                    |   4 +
 Telegram/lib_webrtc                           |   2 +-
 22 files changed, 1278 insertions(+), 209 deletions(-)
 create mode 100644 Telegram/SourceFiles/ui/controls/round_video_recorder.cpp
 create mode 100644 Telegram/SourceFiles/ui/controls/round_video_recorder.h

diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 5ef4af3d6..4f23323ab 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -3502,6 +3502,7 @@ void ApiWrap::sendVoiceMessage(
 		QByteArray result,
 		VoiceWaveform waveform,
 		crl::time duration,
+		bool video,
 		const SendAction &action) {
 	const auto caption = TextWithTags();
 	const auto to = FileLoadTaskOptions(action);
@@ -3510,6 +3511,7 @@ void ApiWrap::sendVoiceMessage(
 		result,
 		duration,
 		waveform,
+		video,
 		to,
 		caption));
 }
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index 7259c410d..18a505de9 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -317,6 +317,7 @@ public:
 		QByteArray result,
 		VoiceWaveform waveform,
 		crl::time duration,
+		bool video,
 		const SendAction &action);
 	void sendFiles(
 		Ui::PreparedList &&list,
diff --git a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp
index 4e77023f2..a122f09a2 100644
--- a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp
+++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp
@@ -73,8 +73,8 @@ private:
 
 	SourceButton _widget;
 	FlatLabel _label;
-	RoundRect _selectedRect;
-	RoundRect _activeRect;
+	Ui::RoundRect _selectedRect;
+	Ui::RoundRect _activeRect;
 	tgcalls::DesktopCaptureSource _source;
 	std::unique_ptr<Preview> _preview;
 	rpl::event_stream<> _activations;
diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp
index 0f7083df3..8fd0f3e97 100644
--- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp
+++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp
@@ -284,10 +284,12 @@ FormatPointer MakeFormatPointer(
 		return {};
 	}
 	result->pb = io.get();
+	result->flags |= AVFMT_FLAG_CUSTOM_IO;
 
 	auto options = (AVDictionary*)nullptr;
 	const auto guard = gsl::finally([&] { av_dict_free(&options); });
 	av_dict_set(&options, "usetoc", "1", 0);
+
 	const auto error = AvErrorWrap(avformat_open_input(
 		&result,
 		nullptr,
@@ -307,6 +309,54 @@ FormatPointer MakeFormatPointer(
 	return FormatPointer(result);
 }
 
+FormatPointer MakeWriteFormatPointer(
+		void *opaque,
+		int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
+#if DA_FFMPEG_CONST_WRITE_CALLBACK
+		int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
+#else
+		int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
+#endif
+		int64_t(*seek)(void *opaque, int64_t offset, int whence),
+		const QByteArray &format) {
+	const AVOutputFormat *found = nullptr;
+	void *i = nullptr;
+	while ((found = av_muxer_iterate(&i))) {
+		if (found->name == format) {
+			break;
+		}
+	}
+	if (!found) {
+		LogError(
+			"av_muxer_iterate",
+			u"Format %1 not found"_q.arg(QString::fromUtf8(format)));
+		return {};
+	}
+
+	auto io = MakeIOPointer(opaque, read, write, seek);
+	if (!io) {
+		return {};
+	}
+	io->seekable = (seek != nullptr);
+
+	auto result = (AVFormatContext*)nullptr;
+	auto error = AvErrorWrap(avformat_alloc_output_context2(
+		&result,
+		(AVOutputFormat*)found,
+		nullptr,
+		nullptr));
+	if (!result || error) {
+		LogError("avformat_alloc_output_context2", error);
+		return {};
+	}
+	result->pb = io.get();
+	result->flags |= AVFMT_FLAG_CUSTOM_IO;
+
+	// Now FormatPointer will own and free the IO context.
+	io.release();
+	return FormatPointer(result);
+}
+
 void FormatDeleter::operator()(AVFormatContext *value) {
 	if (value) {
 		const auto deleter = IOPointer(value->pb);
@@ -448,21 +498,134 @@ SwscalePointer MakeSwscalePointer(
 		existing);
 }
 
+void SwresampleDeleter::operator()(SwrContext *value) {
+	if (value) {
+		swr_free(&value);
+	}
+}
+
+SwresamplePointer MakeSwresamplePointer(
+#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
+		AVChannelLayout *srcLayout,
+#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+		uint64_t srcLayout,
+#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+		AVSampleFormat srcFormat,
+		int srcRate,
+#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
+		AVChannelLayout *dstLayout,
+#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+		uint64_t dstLayout,
+#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+		AVSampleFormat dstFormat,
+		int dstRate,
+		SwresamplePointer *existing) {
+	// We have to use custom caching for SwsContext, because
+	// sws_getCachedContext checks passed flags with existing context flags,
+	// and re-creates context if they're different, but in the process of
+	// context creation the passed flags are modified before being written
+	// to the resulting context, so the caching doesn't work.
+	if (existing && (*existing) != nullptr) {
+		const auto &deleter = existing->get_deleter();
+		if (true
+#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
+			&& srcLayout->nb_channels == deleter.srcChannels
+			&& dstLayout->nb_channels == deleter.dstChannels
+#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+			&& (av_get_channel_layout_nb_channels(srcLayout)
+				== deleter.srcChannels)
+			&& (av_get_channel_layout_nb_channels(dstLayout)
+				== deleter.dstChannels)
+#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+			&& srcFormat == deleter.srcFormat
+			&& dstFormat == deleter.dstFormat
+			&& srcRate == deleter.srcRate
+			&& dstRate == deleter.dstRate) {
+			return std::move(*existing);
+		}
+	}
+
+	// Initialize audio resampler
+#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	auto result = (SwrContext*)nullptr;
+	auto error = AvErrorWrap(swr_alloc_set_opts2(
+		&result,
+		dstLayout,
+		dstFormat,
+		dstRate,
+		srcLayout,
+		srcFormat,
+		srcRate,
+		0,
+		nullptr));
+	if (error || !result) {
+		LogError(u"swr_alloc_set_opts2"_q, error);
+		return SwresamplePointer();
+	}
+#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	auto result = swr_alloc_set_opts(
+		existing ? existing.get() : nullptr,
+		dstLayout,
+		dstFormat,
+		dstRate,
+		srcLayout,
+		srcFormat,
+		srcRate,
+		0,
+		nullptr);
+	if (!result) {
+		LogError(u"swr_alloc_set_opts"_q);
+	}
+#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+
+	error = AvErrorWrap(swr_init(result));
+	if (error) {
+		LogError(u"swr_init"_q, error);
+		swr_free(&result);
+		return SwresamplePointer();
+	}
+
+	return SwresamplePointer(
+		result,
+		{
+			srcFormat,
+			srcRate,
+#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
+			srcLayout->nb_channels,
+#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+			av_get_channel_layout_nb_channels(srcLayout),
+#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+			dstFormat,
+			dstRate,
+#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
+			dstLayout->nb_channels,
+#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+			av_get_channel_layout_nb_channels(dstLayout),
+#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+		});
+}
+
 void SwscaleDeleter::operator()(SwsContext *value) {
 	if (value) {
 		sws_freeContext(value);
 	}
 }
 
-void LogError(const QString &method) {
-	LOG(("Streaming Error: Error in %1.").arg(method));
+void LogError(const QString &method, const QString &details) {
+	LOG(("Streaming Error: Error in %1%2."
+		).arg(method
+		).arg(details.isEmpty() ? QString() : " - " + details));
 }
 
-void LogError(const QString &method, AvErrorWrap error) {
-	LOG(("Streaming Error: Error in %1 (code: %2, text: %3)."
+void LogError(
+		const QString &method,
+		AvErrorWrap error,
+		const QString &details) {
+	LOG(("Streaming Error: Error in %1 (code: %2, text: %3)%4."
 		).arg(method
 		).arg(error.code()
-		).arg(error.text()));
+		).arg(error.text()
+		).arg(details.isEmpty() ? QString() : " - " + details));
 }
 
 crl::time PtsToTime(int64_t pts, AVRational timeBase) {
diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h
index d96daa9c7..6397cbb92 100644
--- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h
+++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h
@@ -19,6 +19,8 @@ extern "C" {
 #include <libavcodec/avcodec.h>
 #include <libavformat/avformat.h>
 #include <libswscale/swscale.h>
+#include <libswresample/swresample.h>
+#include <libavutil/opt.h>
 #include <libavutil/version.h>
 } // extern "C"
 
@@ -138,6 +140,16 @@ using FormatPointer = std::unique_ptr<AVFormatContext, FormatDeleter>;
 	int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
 #endif
 	int64_t(*seek)(void *opaque, int64_t offset, int whence));
+[[nodiscard]] FormatPointer MakeWriteFormatPointer(
+	void *opaque,
+	int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
+#if DA_FFMPEG_CONST_WRITE_CALLBACK
+	int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
+#else
+	int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
+#endif
+	int64_t(*seek)(void *opaque, int64_t offset, int whence),
+	const QByteArray &format);
 
 struct CodecDeleter {
 	void operator()(AVCodecContext *value);
@@ -179,8 +191,39 @@ using SwscalePointer = std::unique_ptr<SwsContext, SwscaleDeleter>;
 	QSize resize,
 	SwscalePointer *existing = nullptr);
 
-void LogError(const QString &method);
-void LogError(const QString &method, FFmpeg::AvErrorWrap error);
+struct SwresampleDeleter {
+	AVSampleFormat srcFormat = AV_SAMPLE_FMT_NONE;
+	int srcRate = 0;
+	int srcChannels = 0;
+	AVSampleFormat dstFormat = AV_SAMPLE_FMT_NONE;
+	int dstRate = 0;
+	int dstChannels = 0;
+
+	void operator()(SwrContext *value);
+};
+using SwresamplePointer = std::unique_ptr<SwrContext, SwresampleDeleter>;
+[[nodiscard]] SwresamplePointer MakeSwresamplePointer(
+#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	AVChannelLayout *srcLayout,
+#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	uint64_t srcLayout,
+#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	AVSampleFormat srcFormat,
+	int srcRate,
+#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	AVChannelLayout *dstLayout,
+#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	uint64_t dstLayout,
+#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	AVSampleFormat dstFormat,
+	int dstRate,
+	SwresamplePointer *existing = nullptr);
+
+void LogError(const QString &method, const QString &details = {});
+void LogError(
+	const QString &method,
+	FFmpeg::AvErrorWrap error,
+	const QString &details = {});
 
 [[nodiscard]] const AVCodec *FindDecoder(not_null<AVCodecContext*> context);
 [[nodiscard]] crl::time PtsToTime(int64_t pts, AVRational timeBase);
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 56c61b71b..7f71f6549 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -1042,6 +1042,7 @@ void HistoryWidget::initVoiceRecordBar() {
 			data.bytes,
 			data.waveform,
 			data.duration,
+			data.video,
 			action);
 		_voiceRecordBar->clearListenState();
 	}, lifetime());
diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h
index e51bae134..bebcc9c57 100644
--- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h
+++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h
@@ -28,6 +28,7 @@ struct VoiceToSend {
 	VoiceWaveform waveform;
 	crl::time duration = 0;
 	Api::SendOptions options;
+	bool video = false;
 };
 struct SendActionUpdate {
 	Api::SendProgressType type = Api::SendProgressType();
diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp
index 837cc0957..7ed5bf0db 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/random.h"
 #include "base/unixtime.h"
 #include "ui/boxes/confirm_box.h"
+#include "calls/calls_instance.h"
 #include "chat_helpers/compose/compose_show.h"
 #include "core/application.h"
 #include "data/data_document.h"
@@ -27,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/audio/media_audio_capture.h"
 #include "media/player/media_player_button.h"
 #include "media/player/media_player_instance.h"
+#include "ui/controls/round_video_recorder.h"
 #include "ui/controls/send_button.h"
 #include "ui/effects/animation_value.h"
 #include "ui/effects/animation_value_f.h"
@@ -37,11 +39,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/widgets/tooltip.h"
 #include "ui/rect.h"
 #include "ui/ui_utility.h"
+#include "webrtc/webrtc_video_track.h"
 #include "styles/style_chat.h"
 #include "styles/style_chat_helpers.h"
 #include "styles/style_layers.h"
 #include "styles/style_media_player.h"
 
+#include <tgcalls/VideoCaptureInterface.h>
+
 namespace HistoryView::Controls {
 namespace {
 
@@ -1579,6 +1584,11 @@ void VoiceRecordBar::activeAnimate(bool active) {
 }
 
 void VoiceRecordBar::visibilityAnimate(bool show, Fn<void()> &&callback) {
+	//if (_videoRecorder) {
+	//	_videoHiding.push_back(base::take(_videoRecorder));
+	//	_videoHiding.back()->hide();
+	//}
+	AssertIsDebug();
 	const auto to = show ? 1. : 0.;
 	const auto from = show ? 0. : 1.;
 	auto animationCallback = [=, callback = std::move(callback)](auto value) {
@@ -1646,12 +1656,17 @@ void VoiceRecordBar::startRecording() {
 	if (isRecording()) {
 		return;
 	}
+	_recordingVideo = true; AssertIsDebug();
 	auto appearanceCallback = [=] {
 		if (_showAnimation.animating()) {
 			return;
 		}
 
 		using namespace ::Media::Capture;
+		if (_recordingVideo && !createVideoRecorder()) {
+			stop(false);
+			return;
+		}
 		if (!instance()->available()) {
 			stop(false);
 			return;
@@ -1664,8 +1679,13 @@ void VoiceRecordBar::startRecording() {
 		if (_paused.current()) {
 			_paused = false;
 			instance()->pause(false, nullptr);
+			if (_videoRecorder) {
+				_videoRecorder->setPaused(false);
+			}
 		} else {
-			instance()->start();
+			instance()->start(_videoRecorder
+				? _videoRecorder->audioChunkProcessor()
+				: nullptr);
 		}
 		instance()->updated(
 		) | rpl::start_with_next_error([=](const Update &update) {
@@ -1769,10 +1789,17 @@ void VoiceRecordBar::hideFast() {
 void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
 	using namespace ::Media::Capture;
 	if (type == StopType::Cancel) {
+		if (_videoRecorder) {
+			_videoRecorder->setPaused(true);
+			_videoRecorder->hide();
+		}
 		instance()->stop(crl::guard(this, [=](Result &&data) {
 			_cancelRequests.fire({});
 		}));
 	} else if (type == StopType::Listen) {
+		if (_videoRecorder) {
+			_videoRecorder->setPaused(true);
+		}
 		instance()->pause(true, crl::guard(this, [=](Result &&data) {
 			if (data.bytes.isEmpty()) {
 				// Close everything.
@@ -1795,6 +1822,29 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
 			// _lockShowing = false;
 		}));
 	} else if (type == StopType::Send) {
+		if (_videoRecorder) {
+			const auto weak = Ui::MakeWeak(this);
+			_videoRecorder->hide([=](Ui::RoundVideoResult data) {
+				crl::on_main([=, data = std::move(data)]() mutable {
+					if (weak) {
+						window()->raise();
+						window()->activateWindow();
+						const auto options = Api::SendOptions{
+							.ttlSeconds = (ttlBeforeHide
+								? std::numeric_limits<int>::max()
+								: 0),
+						};
+						_sendVoiceRequests.fire({
+							data.content,
+							VoiceWaveform{},
+							data.duration,
+							options,
+							true,
+						});
+					}
+				});
+			});
+		}
 		instance()->stop(crl::guard(this, [=](Result &&data) {
 			if (data.bytes.isEmpty()) {
 				// Close everything.
@@ -2094,4 +2144,40 @@ void VoiceRecordBar::showDiscardBox(
 	_warningShown = true;
 }
 
+bool VoiceRecordBar::createVideoRecorder() {
+	if (_videoRecorder) {
+		return true;
+	}
+	const auto hidden = [=](not_null<Ui::RoundVideoRecorder*> which) {
+		if (_videoRecorder.get() == which) {
+			_videoRecorder = nullptr;
+		}
+		_videoHiding.erase(
+			ranges::remove(
+				_videoHiding,
+				which.get(),
+				&std::unique_ptr<Ui::RoundVideoRecorder>::get),
+			end(_videoHiding));
+	};
+	auto capturer = Core::App().calls().getVideoCapture();
+	auto track = std::make_shared<Webrtc::VideoTrack>(
+		Webrtc::VideoState::Active);
+	capturer->setOutput(track->sink());
+	capturer->setPreferredAspectRatio(1.);
+	_videoCapturerLifetime = track->stateValue(
+	) | rpl::start_with_next([=](Webrtc::VideoState state) {
+		capturer->setState((state == Webrtc::VideoState::Active)
+			? tgcalls::VideoState::Active
+			: tgcalls::VideoState::Inactive);
+	});
+	_videoRecorder = std::make_unique<Ui::RoundVideoRecorder>(
+		Ui::RoundVideoRecorderDescriptor{
+			.container = _outerContainer,
+			.hidden = hidden,
+			.capturer = std::move(capturer),
+			.track = std::move(track),
+		});
+	return true;
+}
+
 } // namespace HistoryView::Controls
diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h
index 332bf5b1f..92ac2e6f8 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h
+++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h
@@ -24,6 +24,7 @@ struct RecordBar;
 namespace Ui {
 class AbstractButton;
 class SendButton;
+class RoundVideoRecorder;
 } // namespace Ui
 
 namespace Window {
@@ -124,13 +125,10 @@ private:
 
 	void recordUpdated(quint16 level, int samples);
 
-	[[nodiscard]] bool recordingAnimationCallback(crl::time now);
-
 	void stop(bool send);
 	void stopRecording(StopType type, bool ttlBeforeHide = false);
 	void visibilityAnimate(bool show, Fn<void()> &&callback);
 
-	[[nodiscard]] bool showRecordButton() const;
 	void drawDuration(QPainter &p);
 	void drawRedCircle(QPainter &p);
 	void drawMessage(QPainter &p, float64 recordActive);
@@ -153,6 +151,8 @@ private:
 	[[nodiscard]] bool peekTTLState() const;
 	[[nodiscard]] bool takeTTLState() const;
 
+	[[nodiscard]] bool createVideoRecorder();
+
 	const style::RecordBar &_st;
 	const not_null<Ui::RpWidget*> _outerContainer;
 	const std::shared_ptr<ChatHelpers::Show> _show;
@@ -195,6 +195,11 @@ private:
 	bool _recordingTipRequired = false;
 	bool _lockFromBottom = false;
 
+	std::unique_ptr<Ui::RoundVideoRecorder> _videoRecorder;
+	std::vector<std::unique_ptr<Ui::RoundVideoRecorder>> _videoHiding;
+	rpl::lifetime _videoCapturerLifetime;
+	bool _recordingVideo = false;
+
 	const style::font &_cancelFont;
 
 	rpl::lifetime _recordingLifetime;
diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
index 1ae6f36c5..107e4bf7f 100644
--- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
@@ -1224,6 +1224,7 @@ void RepliesWidget::sendVoice(ComposeControls::VoiceToSend &&data) {
 		data.bytes,
 		data.waveform,
 		data.duration,
+		data.video,
 		std::move(action));
 
 	_composeControls->cancelReplyMessage();
diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
index cb5352f0b..31b31d01d 100644
--- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
@@ -57,29 +57,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace HistoryView {
 
 ScheduledMemento::ScheduledMemento(not_null<History*> history)
-: _history(history)
-, _forumTopic(nullptr) {
+	: _history(history)
+	, _forumTopic(nullptr) {
 	const auto list = _history->session().scheduledMessages().list(_history);
 	if (!list.ids.empty()) {
-		_list.setScrollTopState({ .item = { .fullId = list.ids.front() } });
+		_list.setScrollTopState({ .item = {.fullId = list.ids.front() } });
 	}
 }
 
 ScheduledMemento::ScheduledMemento(not_null<Data::ForumTopic*> forumTopic)
-: _history(forumTopic->owningHistory())
-, _forumTopic(forumTopic) {
+	: _history(forumTopic->owningHistory())
+	, _forumTopic(forumTopic) {
 	const auto list = _history->session().scheduledMessages().list(
 		_forumTopic);
 	if (!list.ids.empty()) {
-		_list.setScrollTopState({ .item = { .fullId = list.ids.front() } });
+		_list.setScrollTopState({ .item = {.fullId = list.ids.front() } });
 	}
 }
 
 object_ptr<Window::SectionWidget> ScheduledMemento::createWidget(
-		QWidget *parent,
-		not_null<Window::SessionController*> controller,
-		Window::Column column,
-		const QRect &geometry) {
+	QWidget *parent,
+	not_null<Window::SessionController*> controller,
+	Window::Column column,
+	const QRect &geometry) {
 	if (column == Window::Column::Third) {
 		return nullptr;
 	}
@@ -97,30 +97,30 @@ ScheduledWidget::ScheduledWidget(
 	not_null<Window::SessionController*> controller,
 	not_null<History*> history,
 	const Data::ForumTopic *forumTopic)
-: Window::SectionWidget(parent, controller, history->peer)
-, WindowListDelegate(controller)
-, _show(controller->uiShow())
-, _history(history)
-, _forumTopic(forumTopic)
-, _scroll(
-	this,
-	controller->chatStyle()->value(lifetime(), st::historyScroll),
-	false)
-, _topBar(this, controller)
-, _topBarShadow(this)
-, _composeControls(std::make_unique<ComposeControls>(
-	this,
-	ComposeControlsDescriptor{
-		.show = controller->uiShow(),
-		.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {
-			listShowPremiumToast(emoji);
-		},
-		.mode = ComposeControls::Mode::Scheduled,
-		.sendMenuDetails = [] { return SendMenu::Details(); },
-		.regularWindow = controller,
-		.stickerOrEmojiChosen = controller->stickerOrEmojiChosen(),
-	}))
-, _cornerButtons(
+	: Window::SectionWidget(parent, controller, history->peer)
+	, WindowListDelegate(controller)
+	, _show(controller->uiShow())
+	, _history(history)
+	, _forumTopic(forumTopic)
+	, _scroll(
+		this,
+		controller->chatStyle()->value(lifetime(), st::historyScroll),
+		false)
+	, _topBar(this, controller)
+	, _topBarShadow(this)
+	, _composeControls(std::make_unique<ComposeControls>(
+		this,
+		ComposeControlsDescriptor{
+			.show = controller->uiShow(),
+			.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {
+				listShowPremiumToast(emoji);
+			},
+			.mode = ComposeControls::Mode::Scheduled,
+			.sendMenuDetails = [] { return SendMenu::Details(); },
+			.regularWindow = controller,
+			.stickerOrEmojiChosen = controller->stickerOrEmojiChosen(),
+		}))
+	, _cornerButtons(
 		_scroll.data(),
 		controller->chatStyle(),
 		static_cast<HistoryView::CornerButtonsDelegate*>(this)) {
@@ -209,83 +209,83 @@ ScheduledWidget::~ScheduledWidget() = default;
 void ScheduledWidget::setupComposeControls() {
 	auto writeRestriction = _forumTopic
 		? [&] {
-			auto topicWriteRestrictions = rpl::single(
-			) | rpl::then(session().changes().topicUpdates(
-				Data::TopicUpdate::Flag::Closed
-			) | rpl::filter([=](const Data::TopicUpdate &update) {
-				return (update.topic->history() == _history)
-					&& (update.topic->rootId() == _forumTopic->rootId());
-			}) | rpl::to_empty) | rpl::map([=] {
-				return (!_forumTopic
-						|| _forumTopic->canToggleClosed()
-						|| !_forumTopic->closed())
-					? std::optional<QString>()
-					: tr::lng_forum_topic_closed(tr::now);
-			});
-			return rpl::combine(
-				session().changes().peerFlagsValue(
-					_history->peer,
-					Data::PeerUpdate::Flag::Rights),
-				Data::CanSendAnythingValue(_history->peer),
-				std::move(topicWriteRestrictions)
-			) | rpl::map([=](
-					auto,
-					auto,
-					std::optional<QString> topicRestriction) {
-				const auto allWithoutPolls = Data::AllSendRestrictions()
-					& ~ChatRestriction::SendPolls;
-				const auto canSendAnything = Data::CanSendAnyOf(
-					_forumTopic,
-					allWithoutPolls);
-				const auto restriction = Data::RestrictionError(
-					_history->peer,
-					ChatRestriction::SendOther);
-				auto text = !canSendAnything
-					? (restriction
-						? restriction
-						: topicRestriction
-						? std::move(topicRestriction)
-						: tr::lng_group_not_accessible(tr::now))
+		auto topicWriteRestrictions = rpl::single(
+		) | rpl::then(session().changes().topicUpdates(
+			Data::TopicUpdate::Flag::Closed
+		) | rpl::filter([=](const Data::TopicUpdate &update) {
+			return (update.topic->history() == _history)
+				&& (update.topic->rootId() == _forumTopic->rootId());
+		}) | rpl::to_empty) | rpl::map([=] {
+			return (!_forumTopic
+				|| _forumTopic->canToggleClosed()
+				|| !_forumTopic->closed())
+				? std::optional<QString>()
+				: tr::lng_forum_topic_closed(tr::now);
+		});
+		return rpl::combine(
+			session().changes().peerFlagsValue(
+				_history->peer,
+				Data::PeerUpdate::Flag::Rights),
+			Data::CanSendAnythingValue(_history->peer),
+			std::move(topicWriteRestrictions)
+		) | rpl::map([=](
+			auto,
+			auto,
+			std::optional<QString> topicRestriction) {
+			const auto allWithoutPolls = Data::AllSendRestrictions()
+				& ~ChatRestriction::SendPolls;
+			const auto canSendAnything = Data::CanSendAnyOf(
+				_forumTopic,
+				allWithoutPolls);
+			const auto restriction = Data::RestrictionError(
+				_history->peer,
+				ChatRestriction::SendOther);
+			auto text = !canSendAnything
+				? (restriction
+					? restriction
 					: topicRestriction
 					? std::move(topicRestriction)
-					: std::optional<QString>();
-				return text ? Controls::WriteRestriction{
-					.text = std::move(*text),
-					.type = Controls::WriteRestrictionType::Rights,
-				} : Controls::WriteRestriction();
-			}) | rpl::type_erased();
-		}()
+					: tr::lng_group_not_accessible(tr::now))
+				: topicRestriction
+				? std::move(topicRestriction)
+				: std::optional<QString>();
+			return text ? Controls::WriteRestriction{
+				.text = std::move(*text),
+				.type = Controls::WriteRestrictionType::Rights,
+			} : Controls::WriteRestriction();
+		}) | rpl::type_erased();
+	}()
 		: [&] {
-			return rpl::combine(
-				session().changes().peerFlagsValue(
-					_history->peer,
-					Data::PeerUpdate::Flag::Rights),
-				Data::CanSendAnythingValue(_history->peer)
-			) | rpl::map([=] {
-				const auto allWithoutPolls = Data::AllSendRestrictions()
-					& ~ChatRestriction::SendPolls;
-				const auto canSendAnything = Data::CanSendAnyOf(
-					_history->peer,
-					allWithoutPolls,
-					false);
-				const auto restriction = Data::RestrictionError(
-					_history->peer,
-					ChatRestriction::SendOther);
-				auto text = !canSendAnything
-					? (restriction
-						? restriction
-						: tr::lng_group_not_accessible(tr::now))
-					: std::optional<QString>();
-				return text ? Controls::WriteRestriction{
-					.text = std::move(*text),
-					.type = Controls::WriteRestrictionType::Rights,
-				} : Controls::WriteRestriction();
-			}) | rpl::type_erased();
-		}();
+		return rpl::combine(
+			session().changes().peerFlagsValue(
+				_history->peer,
+				Data::PeerUpdate::Flag::Rights),
+			Data::CanSendAnythingValue(_history->peer)
+		) | rpl::map([=] {
+			const auto allWithoutPolls = Data::AllSendRestrictions()
+				& ~ChatRestriction::SendPolls;
+			const auto canSendAnything = Data::CanSendAnyOf(
+				_history->peer,
+				allWithoutPolls,
+				false);
+			const auto restriction = Data::RestrictionError(
+				_history->peer,
+				ChatRestriction::SendOther);
+			auto text = !canSendAnything
+				? (restriction
+					? restriction
+					: tr::lng_group_not_accessible(tr::now))
+				: std::optional<QString>();
+			return text ? Controls::WriteRestriction{
+				.text = std::move(*text),
+				.type = Controls::WriteRestrictionType::Rights,
+			} : Controls::WriteRestriction();
+		}) | rpl::type_erased();
+	}();
 	_composeControls->setHistory({
 		.history = _history.get(),
 		.writeRestriction = std::move(writeRestriction),
-	});
+		});
 
 	_composeControls->height(
 	) | rpl::start_with_next([=] {
@@ -308,7 +308,7 @@ void ScheduledWidget::setupComposeControls() {
 
 	_composeControls->sendVoiceRequests(
 	) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) {
-		sendVoice(data.bytes, data.waveform, data.duration);
+		sendVoice(std::move(data));
 	}, lifetime());
 
 	_composeControls->sendCommandRequests(
@@ -393,8 +393,8 @@ void ScheduledWidget::setupComposeControls() {
 	}, lifetime());
 
 	_composeControls->setMimeDataHook([=](
-			not_null<const QMimeData*> data,
-			Ui::InputField::MimeAction action) {
+		not_null<const QMimeData*> data,
+		Ui::InputField::MimeAction action) {
 		if (action == Ui::InputField::MimeAction::Check) {
 			return Core::CanSendFiles(data);
 		} else if (action == Ui::InputField::MimeAction::Insert) {
@@ -426,7 +426,7 @@ void ScheduledWidget::chooseAttach() {
 
 	const auto filter = FileDialog::AllOrImagesFilter();
 	FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=](
-			FileDialog::OpenResult &&result) {
+		FileDialog::OpenResult &&result) {
 		if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
 			return;
 		}
@@ -434,7 +434,7 @@ void ScheduledWidget::chooseAttach() {
 		if (!result.remoteContent.isEmpty()) {
 			auto read = Images::Read({
 				.content = result.remoteContent,
-			});
+				});
 			if (!read.image.isNull() && !read.animated) {
 				confirmSendingFiles(
 					std::move(read.image),
@@ -454,9 +454,9 @@ void ScheduledWidget::chooseAttach() {
 }
 
 bool ScheduledWidget::confirmSendingFiles(
-		not_null<const QMimeData*> data,
-		std::optional<bool> overrideSendImagesAsPhotos,
-		const QString &insertTextOnCancel) {
+	not_null<const QMimeData*> data,
+	std::optional<bool> overrideSendImagesAsPhotos,
+	const QString &insertTextOnCancel) {
 	const auto hasImage = data->hasImage();
 	const auto premium = controller()->session().user()->isPremium();
 
@@ -488,8 +488,8 @@ bool ScheduledWidget::confirmSendingFiles(
 }
 
 bool ScheduledWidget::confirmSendingFiles(
-		Ui::PreparedList &&list,
-		const QString &insertTextOnCancel) {
+	Ui::PreparedList &&list,
+	const QString &insertTextOnCancel) {
 	if (_composeControls->confirmMediaEdit(list)) {
 		return true;
 	} else if (showSendingFilesError(list)) {
@@ -507,11 +507,11 @@ bool ScheduledWidget::confirmSendingFiles(
 		SendMenu::Details());
 
 	box->setConfirmedCallback(crl::guard(this, [=](
-			Ui::PreparedList &&list,
-			Ui::SendFilesWay way,
-			TextWithTags &&caption,
-			Api::SendOptions options,
-			bool ctrlShiftEnter) {
+		Ui::PreparedList &&list,
+		Ui::SendFilesWay way,
+		TextWithTags &&caption,
+		Api::SendOptions options,
+		bool ctrlShiftEnter) {
 		sendingFilesConfirmed(
 			std::move(list),
 			way,
@@ -529,11 +529,11 @@ bool ScheduledWidget::confirmSendingFiles(
 }
 
 void ScheduledWidget::sendingFilesConfirmed(
-		Ui::PreparedList &&list,
-		Ui::SendFilesWay way,
-		TextWithTags &&caption,
-		Api::SendOptions options,
-		bool ctrlShiftEnter) {
+	Ui::PreparedList &&list,
+	Ui::SendFilesWay way,
+	TextWithTags &&caption,
+	Api::SendOptions options,
+	bool ctrlShiftEnter) {
 	Expects(list.filesToProcess.empty());
 
 	if (showSendingFilesError(list, way.sendImagesAsPhotos())) {
@@ -565,10 +565,10 @@ void ScheduledWidget::sendingFilesConfirmed(
 }
 
 bool ScheduledWidget::confirmSendingFiles(
-		QImage &&image,
-		QByteArray &&content,
-		std::optional<bool> overrideSendImagesAsPhotos,
-		const QString &insertTextOnCancel) {
+	QImage &&image,
+	QByteArray &&content,
+	std::optional<bool> overrideSendImagesAsPhotos,
+	const QString &insertTextOnCancel) {
 	if (image.isNull()) {
 		return false;
 	}
@@ -604,8 +604,8 @@ void ScheduledWidget::checkReplyReturns() {
 }
 
 void ScheduledWidget::uploadFile(
-		const QByteArray &fileContent,
-		SendMediaType type) {
+	const QByteArray &fileContent,
+	SendMediaType type) {
 	const auto callback = [=](Api::SendOptions options) {
 		session().api().sendFile(
 			fileContent,
@@ -617,13 +617,13 @@ void ScheduledWidget::uploadFile(
 }
 
 bool ScheduledWidget::showSendingFilesError(
-		const Ui::PreparedList &list) const {
+	const Ui::PreparedList &list) const {
 	return showSendingFilesError(list, std::nullopt);
 }
 
 bool ScheduledWidget::showSendingFilesError(
-		const Ui::PreparedList &list,
-		std::optional<bool> compress) const {
+	const Ui::PreparedList &list,
+	std::optional<bool> compress) const {
 	const auto text = [&] {
 		using Error = Ui::PreparedList::Error;
 		const auto peer = _history->peer;
@@ -656,7 +656,7 @@ bool ScheduledWidget::showSendingFilesError(
 }
 
 Api::SendAction ScheduledWidget::prepareSendAction(
-		Api::SendOptions options) const {
+	Api::SendOptions options) const {
 	auto result = Api::SendAction(_history, options);
 	result.options.sendAs = _composeControls->sendAsPeer();
 	if (_forumTopic) {
@@ -716,26 +716,22 @@ void ScheduledWidget::send(Api::SendOptions options) {
 	_composeControls->focus();
 }
 
-void ScheduledWidget::sendVoice(
-		QByteArray bytes,
-		VoiceWaveform waveform,
-		crl::time duration) {
+void ScheduledWidget::sendVoice(const Controls::VoiceToSend &data) {
 	const auto callback = [=](Api::SendOptions options) {
-		sendVoice(bytes, waveform, duration, options);
+		sendVoice(base::duplicate(data), options);
 	};
 	controller()->show(
 		PrepareScheduleBox(this, _show, sendMenuDetails(), callback));
 }
 
 void ScheduledWidget::sendVoice(
-		QByteArray bytes,
-		VoiceWaveform waveform,
-		crl::time duration,
+		const Controls::VoiceToSend &data,
 		Api::SendOptions options) {
 	session().api().sendVoiceMessage(
-		bytes,
-		waveform,
-		duration,
+		data.bytes,
+		data.waveform,
+		data.duration,
+		data.video,
 		prepareSendAction(options));
 	_composeControls->clearListenState();
 }
diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h
index 0a7ff39b0..290b11c73 100644
--- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h
+++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h
@@ -47,6 +47,10 @@ namespace InlineBots {
 class Result;
 } // namespace InlineBots
 
+namespace HistoryView::Controls {
+struct VoiceToSend;
+} // namespace HistoryView::Controls
+
 namespace HistoryView {
 
 class Element;
@@ -207,14 +211,9 @@ private:
 		Api::SendOptions options) const;
 	void send();
 	void send(Api::SendOptions options);
+	void sendVoice(const Controls::VoiceToSend &data);
 	void sendVoice(
-		QByteArray bytes,
-		VoiceWaveform waveform,
-		crl::time duration);
-	void sendVoice(
-		QByteArray bytes,
-		VoiceWaveform waveform,
-		crl::time duration,
+		const Controls::VoiceToSend &data,
 		Api::SendOptions options);
 	void edit(
 		not_null<HistoryItem*> item,
diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp
index 1bbf9b10c..cd567f4fd 100644
--- a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp
+++ b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp
@@ -88,13 +88,15 @@ public:
 	void start(
 		Webrtc::DeviceResolvedId id,
 		Fn<void(Update)> updated,
-		Fn<void()> error);
+		Fn<void()> error,
+		Fn<void(Chunk)> externalProcessing);
 	void stop(Fn<void(Result&&)> callback = nullptr);
 	void pause(bool value, Fn<void(Result&&)> callback);
 
 private:
 	void process();
 
+	bool initializeFFmpeg();
 	[[nodiscard]] bool processFrame(int32 offset, int32 framesize);
 	void fail();
 
@@ -104,6 +106,7 @@ private:
 	// Returns number of packets written or -1 on error
 	[[nodiscard]] int writePackets();
 
+	Fn<void(Chunk)> _externalProcessing;
 	Fn<void(Update)> _updated;
 	Fn<void()> _error;
 
@@ -131,7 +134,7 @@ Instance::Instance() : _inner(std::make_unique<Inner>(&_thread)) {
 	_thread.start();
 }
 
-void Instance::start() {
+void Instance::start(Fn<void(Chunk)> externalProcessing) {
 	_updates.fire_done();
 	const auto id = Audio::Current().captureDeviceId();
 	InvokeQueued(_inner.get(), [=] {
@@ -143,7 +146,7 @@ void Instance::start() {
 			crl::on_main(this, [=] {
 				_updates.fire_error({});
 			});
-		});
+		}, externalProcessing);
 		crl::on_main(this, [=] {
 			_started = true;
 		});
@@ -304,7 +307,9 @@ void Instance::Inner::fail() {
 void Instance::Inner::start(
 		Webrtc::DeviceResolvedId id,
 		Fn<void(Update)> updated,
-		Fn<void()> error) {
+		Fn<void()> error,
+		Fn<void(Chunk)> externalProcessing) {
+	_externalProcessing = std::move(externalProcessing);
 	_updated = std::move(updated);
 	_error = std::move(error);
 	if (_paused) {
@@ -329,8 +334,19 @@ void Instance::Inner::start(
 		d->device = nullptr;
 		fail();
 		return;
+	} else if (!_externalProcessing) {
+		if (!initializeFFmpeg()) {
+			fail();
+			return;
+		}
 	}
+	_timer.callEach(50);
+	_captured.clear();
+	_captured.reserve(kCaptureBufferSlice);
+	DEBUG_LOG(("Audio Capture: started!"));
+}
 
+bool Instance::Inner::initializeFFmpeg() {
 	// Create encoding context
 
 	d->ioBuffer = (uchar*)av_malloc(FFmpeg::kAVBlockSize);
@@ -347,14 +363,12 @@ void Instance::Inner::start(
 	}
 	if (!fmt) {
 		LOG(("Audio Error: Unable to find opus AVOutputFormat for capture"));
-		fail();
-		return;
+		return false;
 	}
 
 	if ((res = avformat_alloc_output_context2(&d->fmtContext, (AVOutputFormat*)fmt, 0, 0)) < 0) {
 		LOG(("Audio Error: Unable to avformat_alloc_output_context2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
-		fail();
-		return;
+		return false;
 	}
 	d->fmtContext->pb = d->ioContext;
 	d->fmtContext->flags |= AVFMT_FLAG_CUSTOM_IO;
@@ -364,21 +378,18 @@ void Instance::Inner::start(
 	d->codec = avcodec_find_encoder(fmt->audio_codec);
 	if (!d->codec) {
 		LOG(("Audio Error: Unable to avcodec_find_encoder for capture"));
-		fail();
-		return;
+		return false;
 	}
 	d->stream = avformat_new_stream(d->fmtContext, d->codec);
 	if (!d->stream) {
 		LOG(("Audio Error: Unable to avformat_new_stream for capture"));
-		fail();
-		return;
+		return false;
 	}
 	d->stream->id = d->fmtContext->nb_streams - 1;
 	d->codecContext = avcodec_alloc_context3(d->codec);
 	if (!d->codecContext) {
 		LOG(("Audio Error: Unable to avcodec_alloc_context3 for capture"));
-		fail();
-		return;
+		return false;
 	}
 
 	av_opt_set_int(d->codecContext, "refcounted_frames", 1, 0);
@@ -401,8 +412,7 @@ void Instance::Inner::start(
 	// Open audio stream
 	if ((res = avcodec_open2(d->codecContext, d->codec, nullptr)) < 0) {
 		LOG(("Audio Error: Unable to avcodec_open2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
-		fail();
-		return;
+		return false;
 	}
 
 	// Alloc source samples
@@ -443,39 +453,27 @@ void Instance::Inner::start(
 #endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
 	if (res < 0 || !d->swrContext) {
 		LOG(("Audio Error: Unable to swr_alloc_set_opts2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
-		fail();
-		return;
+		return false;
 	} else if ((res = swr_init(d->swrContext)) < 0) {
 		LOG(("Audio Error: Unable to swr_init for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
-		fail();
-		return;
+		return false;
 	}
-
 	d->maxDstSamples = d->srcSamples;
 	if ((res = av_samples_alloc_array_and_samples(&d->dstSamplesData, 0, d->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0)) < 0) {
 		LOG(("Audio Error: Unable to av_samples_alloc_array_and_samples for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
-		fail();
-		return;
+		return false;
 	}
 	d->dstSamplesSize = av_samples_get_buffer_size(0, d->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0);
-
 	if ((res = avcodec_parameters_from_context(d->stream->codecpar, d->codecContext)) < 0) {
 		LOG(("Audio Error: Unable to avcodec_parameters_from_context for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
-		fail();
-		return;
+		return false;
 	}
-
 	// Write file header
 	if ((res = avformat_write_header(d->fmtContext, 0)) < 0) {
 		LOG(("Audio Error: Unable to avformat_write_header for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
-		fail();
-		return;
+		return false;
 	}
-
-	_timer.callEach(50);
-	_captured.clear();
-	_captured.reserve(kCaptureBufferSlice);
-	DEBUG_LOG(("Audio Capture: started!"));
+	return true;
 }
 
 void Instance::Inner::pause(bool value, Fn<void(Result&&)> callback) {
@@ -559,7 +557,7 @@ void Instance::Inner::stop(Fn<void(Result&&)> callback) {
 	_captured = QByteArray();
 
 	// Finish stream
-	if (needResult && hadDevice) {
+	if (needResult && hadDevice && d->fmtContext) {
 		av_write_trailer(d->fmtContext);
 	}
 
@@ -658,6 +656,13 @@ void Instance::Inner::process() {
 		if (ErrorHappened(d->device)) {
 			fail();
 			return;
+		} else if (_externalProcessing) {
+			_externalProcessing({
+				.finished = crl::now(),
+				.samples = base::take(_captured),
+				.frequency = kCaptureFrequency,
+			});
+			return;
 		}
 
 		// Count new recording level and update view
diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.h b/Telegram/SourceFiles/media/audio/media_audio_capture.h
index 2b46bd7fa..fe03dd89c 100644
--- a/Telegram/SourceFiles/media/audio/media_audio_capture.h
+++ b/Telegram/SourceFiles/media/audio/media_audio_capture.h
@@ -7,10 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
+#include <QtCore/QThread>
 #include <QtCore/QTimer>
 
-struct AVFrame;
-
 namespace Media {
 namespace Capture {
 
@@ -19,6 +18,12 @@ struct Update {
 	ushort level = 0;
 };
 
+struct Chunk {
+	crl::time finished = 0;
+	QByteArray samples;
+	int frequency = 0;
+};
+
 struct Result;
 
 void Start();
@@ -45,7 +50,7 @@ public:
 		return _started.changes();
 	}
 
-	void start();
+	void start(Fn<void(Chunk)> externalProcessing = nullptr);
 	void stop(Fn<void(Result&&)> callback = nullptr);
 	void pause(bool value, Fn<void(Result&&)> callback);
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index 136c77b7a..c034d61dc 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -244,6 +244,7 @@ void ReplyArea::sendVoice(VoiceToSend &&data) {
 		data.bytes,
 		data.waveform,
 		data.duration,
+		data.video,
 		std::move(action));
 
 	_controls->clearListenState();
diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp
index 1610aafee..d1dfbbad5 100644
--- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp
+++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp
@@ -1200,6 +1200,7 @@ void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) {
 		data.bytes,
 		data.waveform,
 		data.duration,
+		data.video,
 		std::move(action));
 
 	_composeControls->cancelReplyMessage();
diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp
index 79c4af401..9daeaecdd 100644
--- a/Telegram/SourceFiles/storage/localimageloader.cpp
+++ b/Telegram/SourceFiles/storage/localimageloader.cpp
@@ -498,6 +498,7 @@ FileLoadTask::FileLoadTask(
 	const QByteArray &voice,
 	crl::time duration,
 	const VoiceWaveform &waveform,
+	bool video,
 	const FileLoadTo &to,
 	const TextWithTags &caption)
 : _id(base::RandomValue<uint64>())
@@ -507,7 +508,7 @@ FileLoadTask::FileLoadTask(
 , _content(voice)
 , _duration(duration)
 , _waveform(waveform)
-, _type(SendMediaType::Audio)
+, _type(video ? SendMediaType::Round : SendMediaType::Audio)
 , _caption(caption) {
 }
 
@@ -696,6 +697,7 @@ void FileLoadTask::process(Args &&args) {
 	auto isSong = false;
 	auto isVideo = false;
 	auto isVoice = (_type == SendMediaType::Audio);
+	auto isRound = (_type == SendMediaType::Round);
 	auto isSticker = false;
 
 	auto fullimage = QImage();
@@ -711,7 +713,7 @@ void FileLoadTask::process(Args &&args) {
 		// Voice sending is supported only from memory for now.
 		// Because for voice we force mime type and don't read MediaInformation.
 		// For a real file we always read mime type and read MediaInformation.
-		Assert(!isVoice);
+		Assert(!isVoice && !isRound);
 
 		filesize = info.size();
 		filename = info.fileName();
@@ -736,6 +738,9 @@ void FileLoadTask::process(Args &&args) {
 		if (isVoice) {
 			filename = filedialogDefaultName(u"audio"_q, u".ogg"_q, QString(), true);
 			filemime = "audio/ogg";
+		} else if (isRound) {
+			filename = filedialogDefaultName(u"round"_q, u".mp4"_q, QString(), true);
+			filemime = "video/mp4";
 		} else {
 			if (_information) {
 				if (auto image = std::get_if<Ui::PreparedFileInformation::Image>(
@@ -815,7 +820,41 @@ void FileLoadTask::process(Args &&args) {
 	auto photo = MTP_photoEmpty(MTP_long(0));
 	auto document = MTP_documentEmpty(MTP_long(0));
 
-	if (!isVoice) {
+	if (isRound) {
+		_information = readMediaInformation(u"video/mp4"_q);
+		if (auto video = std::get_if<Ui::PreparedFileInformation::Video>(
+			&_information->media)) {
+			isVideo = true;
+			auto coverWidth = video->thumbnail.width();
+			auto coverHeight = video->thumbnail.height();
+			if (video->isGifv && !_album) {
+				attributes.push_back(MTP_documentAttributeAnimated());
+			}
+			auto flags = MTPDdocumentAttributeVideo::Flags(
+				MTPDdocumentAttributeVideo::Flag::f_round_message);
+			if (video->supportsStreaming) {
+				flags |= MTPDdocumentAttributeVideo::Flag::f_supports_streaming;
+			}
+			const auto realSeconds = video->duration / 1000.;
+			attributes.push_back(MTP_documentAttributeVideo(
+				MTP_flags(flags),
+				MTP_double(realSeconds),
+				MTP_int(coverWidth),
+				MTP_int(coverHeight),
+				MTPint(), // preload_prefix_size
+				MTPdouble(), // video_start_ts
+				MTPstring())); // video_codec
+
+			if (args.generateGoodThumbnail) {
+				goodThumbnail = video->thumbnail;
+				{
+					QBuffer buffer(&goodThumbnailBytes);
+					goodThumbnail.save(&buffer, "JPG", kThumbnailQuality);
+				}
+			}
+			thumbnail = PrepareFileThumbnail(std::move(video->thumbnail));
+		}
+	} else if (!isVoice) {
 		if (!_information) {
 			_information = readMediaInformation(filemime);
 			filemime = _information->filemime;
@@ -869,7 +908,7 @@ void FileLoadTask::process(Args &&args) {
 		}
 	}
 
-	if (!fullimage.isNull() && fullimage.width() > 0 && !isSong && !isVideo && !isVoice) {
+	if (!fullimage.isNull() && fullimage.width() > 0 && !isSong && !isVideo && !isVoice && !isRound) {
 		auto w = fullimage.width(), h = fullimage.height();
 		attributes.push_back(MTP_documentAttributeImageSize(MTP_int(w), MTP_int(h)));
 
diff --git a/Telegram/SourceFiles/storage/localimageloader.h b/Telegram/SourceFiles/storage/localimageloader.h
index d4e99177f..ed0dc1c47 100644
--- a/Telegram/SourceFiles/storage/localimageloader.h
+++ b/Telegram/SourceFiles/storage/localimageloader.h
@@ -31,6 +31,7 @@ extern const char kOptionSendLargePhotos[];
 enum class SendMediaType {
 	Photo,
 	Audio,
+	Round,
 	File,
 	ThemeFile,
 	Secure,
@@ -231,6 +232,7 @@ public:
 		const QByteArray &voice,
 		crl::time duration,
 		const VoiceWaveform &waveform,
+		bool video,
 		const FileLoadTo &to,
 		const TextWithTags &caption);
 	~FileLoadTask();
diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp
new file mode 100644
index 000000000..09b60bb7b
--- /dev/null
+++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp
@@ -0,0 +1,649 @@
+/*
+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
+*/
+#include "ui/controls/round_video_recorder.h"
+
+#include "base/debug_log.h"
+#include "ffmpeg/ffmpeg_utility.h"
+#include "media/audio/media_audio_capture.h"
+#include "ui/painter.h"
+#include "ui/rp_widget.h"
+#include "webrtc/webrtc_video_track.h"
+#include "styles/style_chat_helpers.h"
+
+namespace Ui {
+namespace {
+
+constexpr auto kSide = 400;
+constexpr auto kOutputFilename = "C:\\Tmp\\TestVideo\\output.mp4";
+
+using namespace FFmpeg;
+
+} // namespace
+
+class RoundVideoRecorder::Private final {
+public:
+	Private(crl::weak_on_queue<Private> weak);
+	~Private();
+
+	void push(int64 mcstimestamp, const QImage &frame);
+	void push(const Media::Capture::Chunk &chunk);
+
+	[[nodiscard]] RoundVideoResult finish();
+
+private:
+	static int Write(void *opaque, uint8_t *buf, int buf_size);
+	static int64_t Seek(void *opaque, int64_t offset, int whence);
+
+	int write(uint8_t *buf, int buf_size);
+	int64_t seek(int64_t offset, int whence);
+
+	const crl::weak_on_queue<Private> _weak;
+
+	FormatPointer _format;
+
+	AVStream *_videoStream = nullptr;
+	CodecPointer _videoCodec;
+	FramePointer _videoFrame;
+	SwscalePointer _swsContext;
+	int64_t _videoPts = 0;
+
+	// This is the first recorded frame timestamp in microseconds.
+	int64_t _videoFirstTimestamp = -1;
+
+	// Audio-related members
+	AVStream *_audioStream = nullptr;
+	CodecPointer _audioCodec;
+	FramePointer _audioFrame;
+	SwresamplePointer _swrContext;
+	QByteArray _audioTail;
+	int64_t _audioPts = 0;
+	int _audioChannels = 0;
+
+	// Those timestamps are in 'ms' used for sync between audio and video.
+	crl::time _firstAudioChunkFinished = 0;
+	crl::time _firstVideoFrameTime = 0;
+
+	QByteArray _result;
+	int64_t _resultOffset = 0;
+	crl::time _resultDuration = 0;
+
+	void initEncoding();
+	bool initVideo();
+	bool initAudio();
+	void deinitEncoding();
+	void finishEncoding();
+
+	void encodeVideoFrame(int64 mcstimestamp, const QImage &frame);
+	void encodeAudioFrame(const Media::Capture::Chunk &chunk);
+	bool writeFrame(
+		const FramePointer &frame,
+		const CodecPointer &codec,
+		AVStream *stream);
+
+};
+
+RoundVideoRecorder::Private::Private(crl::weak_on_queue<Private> weak)
+: _weak(std::move(weak)) {
+	initEncoding();
+}
+
+RoundVideoRecorder::Private::~Private() {
+	finishEncoding();
+
+	QFile file(kOutputFilename);
+	if (file.open(QIODevice::WriteOnly)) {
+		file.write(_result);
+	}
+}
+
+int RoundVideoRecorder::Private::Write(void *opaque, uint8_t *buf, int buf_size) {
+	return static_cast<Private*>(opaque)->write(buf, buf_size);
+}
+
+int64_t RoundVideoRecorder::Private::Seek(void *opaque, int64_t offset, int whence) {
+	return static_cast<Private*>(opaque)->seek(offset, whence);
+}
+
+int RoundVideoRecorder::Private::write(uint8_t *buf, int buf_size) {
+	if (const auto total = _resultOffset + int64(buf_size)) {
+		const auto size = int64(_result.size());
+		constexpr auto kReserve = 1024 * 1024;
+		_result.reserve((total / kReserve) * kReserve);
+		const auto overwrite = std::min(
+			size - _resultOffset,
+			int64(buf_size));
+		if (overwrite) {
+			memcpy(_result.data() + _resultOffset, buf, overwrite);
+		}
+		if (const auto append = buf_size - overwrite) {
+			_result.append(
+				reinterpret_cast<const char*>(buf) + overwrite,
+				append);
+		}
+		_resultOffset += buf_size;
+	}
+	return buf_size;
+}
+
+int64_t RoundVideoRecorder::Private::seek(int64_t offset, int whence) {
+	const auto checkedSeek = [&](int64_t offset) {
+		if (offset < 0 || offset > int64(_result.size())) {
+			return int64(-1);
+		}
+		return (_resultOffset = offset);
+	};
+	switch (whence) {
+	case SEEK_SET: return checkedSeek(offset);
+	case SEEK_CUR: return checkedSeek(_resultOffset + offset);
+	case SEEK_END: return checkedSeek(int64(_result.size()) + offset);
+	case AVSEEK_SIZE: return int64(_result.size());
+	}
+	return -1;
+}
+
+void RoundVideoRecorder::Private::initEncoding() {
+	_format = MakeWriteFormatPointer(
+		static_cast<void*>(this),
+		nullptr,
+		&Private::Write,
+		&Private::Seek,
+		"mp4"_q);
+
+	if (!initVideo() || !initAudio()) {
+		deinitEncoding();
+		return;
+	}
+
+	const auto error = AvErrorWrap(avformat_write_header(
+		_format.get(),
+		nullptr));
+	if (error) {
+		LogError("avformat_write_header", error);
+		deinitEncoding();
+	}
+}
+
+bool RoundVideoRecorder::Private::initVideo() {
+	if (!_format) {
+		return false;
+	}
+
+	const auto videoCodec = avcodec_find_encoder_by_name("libopenh264");
+	if (!videoCodec) {
+		LogError("avcodec_find_encoder_by_name", "libopenh264");
+		return false;
+	}
+
+	_videoStream = avformat_new_stream(_format.get(), videoCodec);
+	if (!_videoStream) {
+		LogError("avformat_new_stream", "libopenh264");
+		return false;
+	}
+
+	_videoCodec = CodecPointer(avcodec_alloc_context3(videoCodec));
+	if (!_videoCodec) {
+		LogError("avcodec_alloc_context3", "libopenh264");
+		return false;
+	}
+
+	_videoCodec->codec_id = videoCodec->id;
+	_videoCodec->codec_type = AVMEDIA_TYPE_VIDEO;
+	_videoCodec->width = kSide;
+	_videoCodec->height = kSide;
+	_videoCodec->time_base = AVRational{ 1, 1'000'000 }; // Microseconds.
+	_videoCodec->framerate = AVRational{ 0, 1 }; // Variable frame rate.
+	_videoCodec->pix_fmt = AV_PIX_FMT_YUV420P;
+	_videoCodec->bit_rate = 5 * 1024 * 1024; // 5Mbps
+
+	auto error = AvErrorWrap(avcodec_open2(
+		_videoCodec.get(),
+		videoCodec,
+		nullptr));
+	if (error) {
+		LogError("avcodec_open2", error, "libopenh264");
+		return false;
+	}
+
+	error = AvErrorWrap(avcodec_parameters_from_context(
+		_videoStream->codecpar,
+		_videoCodec.get()));
+	if (error) {
+		LogError("avcodec_parameters_from_context", error, "libopenh264");
+		return false;
+	}
+
+	_videoFrame = MakeFramePointer();
+	if (!_videoFrame) {
+		return false;
+	}
+
+	_videoFrame->format = _videoCodec->pix_fmt;
+	_videoFrame->width = _videoCodec->width;
+	_videoFrame->height = _videoCodec->height;
+
+	error = AvErrorWrap(av_frame_get_buffer(_videoFrame.get(), 0));
+	if (error) {
+		LogError("av_frame_get_buffer", error, "libopenh264");
+		return false;
+	}
+
+	return true;
+}
+
+bool RoundVideoRecorder::Private::initAudio() {
+	if (!_format) {
+		return false;
+	}
+
+	const auto audioCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
+	if (!audioCodec) {
+		LogError("avcodec_find_encoder", "AAC");
+		return false;
+	}
+
+	_audioStream = avformat_new_stream(_format.get(), audioCodec);
+	if (!_audioStream) {
+		LogError("avformat_new_stream", "AAC");
+		return false;
+	}
+
+	_audioCodec = CodecPointer(avcodec_alloc_context3(audioCodec));
+	if (!_audioCodec) {
+		LogError("avcodec_alloc_context3", "AAC");
+		return false;
+	}
+
+	_audioChannels = 1;
+	_audioCodec->sample_fmt = AV_SAMPLE_FMT_FLTP;
+	_audioCodec->bit_rate = 32000;
+	_audioCodec->sample_rate = 48000;
+#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	_audioCodec->ch_layout = AV_CHANNEL_LAYOUT_MONO;
+	_audioCodec->channels = _audioCodec->ch_layout.nb_channels;
+#else
+	_audioCodec->channel_layout = AV_CH_LAYOUT_MONO;
+	_audioCodec->channels = _audioChannels;
+#endif
+
+	auto error = AvErrorWrap(avcodec_open2(
+		_audioCodec.get(),
+		audioCodec,
+		nullptr));
+	if (error) {
+		LogError("avcodec_open2", error, "AAC");
+		return false;
+	}
+
+	error = AvErrorWrap(avcodec_parameters_from_context(
+		_audioStream->codecpar,
+		_audioCodec.get()));
+	if (error) {
+		LogError("avcodec_parameters_from_context", error, "AAC");
+		return false;
+	}
+
+#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	_swrContext = MakeSwresamplePointer(
+		&_audioCodec->ch_layout,
+		AV_SAMPLE_FMT_S16,
+		_audioCodec->sample_rate,
+		&_audioCodec->ch_layout,
+		_audioCodec->sample_fmt,
+		_audioCodec->sample_rate,
+		&_swrContext);
+#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	_swrContext = MakeSwresamplePointer(
+		&_audioCodec->ch_layout,
+		AV_SAMPLE_FMT_S16,
+		_audioCodec->sample_rate,
+		&_audioCodec->ch_layout,
+		_audioCodec->sample_fmt,
+		_audioCodec->sample_rate,
+		&_swrContext);
+#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	if (!_swrContext) {
+		return false;
+	}
+
+	_audioFrame = MakeFramePointer();
+	if (!_audioFrame) {
+		return false;
+	}
+
+	_audioFrame->nb_samples = _audioCodec->frame_size;
+	_audioFrame->format = _audioCodec->sample_fmt;
+	_audioFrame->sample_rate = _audioCodec->sample_rate;
+#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
+	av_channel_layout_copy(&_audioFrame->ch_layout, &_audioCodec->ch_layout);
+#else
+	_audioFrame->channel_layout = _audioCodec->channel_layout;
+	_audioFrame->channels = _audioCodec->channels;
+#endif
+
+	error = AvErrorWrap(av_frame_get_buffer(_audioFrame.get(), 0));
+	if (error) {
+		LogError("av_frame_get_buffer", error, "AAC");
+		return false;
+	}
+
+	return true;
+}
+
+void RoundVideoRecorder::Private::finishEncoding() {
+	if (_format
+		&& writeFrame(nullptr, _videoCodec, _videoStream)
+		&& writeFrame(nullptr, _audioCodec, _audioStream)) {
+		av_write_trailer(_format.get());
+	}
+	deinitEncoding();
+}
+
+RoundVideoResult RoundVideoRecorder::Private::finish() {
+	if (!_format) {
+		return {};
+	}
+	finishEncoding();
+	return {
+		.content = _result,
+		.waveform = QByteArray(),
+		.duration = _resultDuration,
+	};
+};
+
+void RoundVideoRecorder::Private::deinitEncoding() {
+	_swsContext = nullptr;
+	_videoCodec = nullptr;
+	_videoStream = nullptr;
+	_videoFrame = nullptr;
+	_swrContext = nullptr;
+	_audioCodec = nullptr;
+	_audioStream = nullptr;
+	_audioFrame = nullptr;
+	_format = nullptr;
+
+	_videoFirstTimestamp = -1;
+	_videoPts = 0;
+	_audioPts = 0;
+}
+
+void RoundVideoRecorder::Private::push(
+		int64 mcstimestamp,
+		const QImage &frame) {
+	if (!_format) {
+		return;
+	} else if (!_firstAudioChunkFinished) {
+		// Skip frames while we didn't start receiving audio.
+		return;
+	} else if (!_firstVideoFrameTime) {
+		_firstVideoFrameTime = crl::now();
+	}
+	encodeVideoFrame(mcstimestamp, frame);
+}
+
+void RoundVideoRecorder::Private::push(const Media::Capture::Chunk &chunk) {
+	if (!_format) {
+		return;
+	} else if (!_firstAudioChunkFinished || !_firstVideoFrameTime) {
+		_firstAudioChunkFinished = chunk.finished;
+		return;
+	}
+	// We get a chunk roughly every 50ms and need to encode it interleaved.
+	encodeAudioFrame(chunk);
+}
+
+void RoundVideoRecorder::Private::encodeVideoFrame(
+		int64 mcstimestamp,
+		const QImage &frame) {
+	_swsContext = MakeSwscalePointer(
+		QSize(kSide, kSide),
+		AV_PIX_FMT_BGRA,
+		QSize(kSide, kSide),
+		AV_PIX_FMT_YUV420P,
+		&_swsContext);
+	if (!_swsContext) {
+		deinitEncoding();
+		return;
+	}
+
+	if (_videoFirstTimestamp == -1) {
+		_videoFirstTimestamp = mcstimestamp;
+	}
+
+	const auto fwidth = frame.width();
+	const auto fheight = frame.height();
+	const auto fmin = std::min(fwidth, fheight);
+	const auto fx = (fwidth > fheight) ? (fwidth - fheight) / 2 : 0;
+	const auto fy = (fwidth < fheight) ? (fheight - fwidth) / 2 : 0;
+	const auto crop = QRect(fx, fy, fmin, fmin);
+	const auto cropped = frame.copy(crop).scaled(
+		kSide,
+		kSide,
+		Qt::KeepAspectRatio,
+		Qt::SmoothTransformation);
+
+	// Convert QImage to RGB32 format
+//	QImage rgbImage = cropped.convertToFormat(QImage::Format_ARGB32);
+
+	// Prepare source data
+	const uint8_t *srcSlice[1] = { cropped.constBits() };
+	int srcStride[1] = { cropped.bytesPerLine() };
+
+	// Perform the color space conversion
+	sws_scale(
+		_swsContext.get(),
+		srcSlice,
+		srcStride,
+		0,
+		kSide,
+		_videoFrame->data,
+		_videoFrame->linesize);
+
+	_videoFrame->pts = mcstimestamp - _videoFirstTimestamp;
+
+	LOG(("Audio At: %1").arg(_videoFrame->pts / 1'000'000.));
+	if (!writeFrame(_videoFrame, _videoCodec, _videoStream)) {
+		return;
+	}
+}
+
+void RoundVideoRecorder::Private::encodeAudioFrame(const Media::Capture::Chunk &chunk) {
+	if (_audioTail.isEmpty()) {
+		_audioTail = chunk.samples;
+	} else {
+		_audioTail.append(chunk.samples);
+	}
+
+	const int inSamples = _audioTail.size() / sizeof(int16_t);
+	const uint8_t *inData = reinterpret_cast<const uint8_t*>(_audioTail.constData());
+	int samplesProcessed = 0;
+
+	while (samplesProcessed + _audioCodec->frame_size <= inSamples) {
+		int remainingSamples = inSamples - samplesProcessed;
+		int outSamples = av_rescale_rnd(
+			swr_get_delay(_swrContext.get(), 48000) + remainingSamples,
+			_audioCodec->sample_rate,
+			48000,
+			AV_ROUND_UP);
+
+		// Ensure we don't exceed the frame's capacity
+		outSamples = std::min(outSamples, _audioCodec->frame_size);
+
+		const auto process = std::min(remainingSamples, outSamples);
+		auto dataptr = inData + samplesProcessed * sizeof(int16_t);
+		auto error = AvErrorWrap(swr_convert(
+			_swrContext.get(),
+			_audioFrame->data,
+			outSamples,
+			&dataptr,
+			process));
+
+		if (error) {
+			LogError("swr_convert", error);
+			deinitEncoding();
+			return;
+		}
+
+		// Update the actual number of samples in the frame
+		_audioFrame->nb_samples = error.code();
+
+		_audioFrame->pts = _audioPts;
+		_audioPts += _audioFrame->nb_samples;
+
+		LOG(("Audio At: %1").arg(_audioFrame->pts / 48'000.));
+		if (!writeFrame(_audioFrame, _audioCodec, _audioStream)) {
+			return;
+		}
+
+		samplesProcessed += process;
+	}
+	const auto left = inSamples - samplesProcessed;
+	if (left > 0) {
+		memmove(_audioTail.data(), _audioTail.data() + samplesProcessed * sizeof(int16_t), left * sizeof(int16_t));
+		_audioTail.resize(left * sizeof(int16_t));
+	} else {
+		_audioTail.clear();
+	}
+}
+
+bool RoundVideoRecorder::Private::writeFrame(
+		const FramePointer &frame,
+		const CodecPointer &codec,
+		AVStream *stream) {
+	auto error = AvErrorWrap(avcodec_send_frame(codec.get(), frame.get()));
+	if (error) {
+		LogError("avcodec_send_frame", error);
+		deinitEncoding();
+		return false;
+	}
+
+	auto pkt = av_packet_alloc();
+	const auto guard = gsl::finally([&] {
+		av_packet_free(&pkt);
+	});
+	while (true) {
+		error = AvErrorWrap(avcodec_receive_packet(codec.get(), pkt));
+		if (error.code() == AVERROR(EAGAIN)) {
+			return true;  // Need more input
+		} else if (error.code() == AVERROR_EOF) {
+			return true;  // Encoding finished
+		} else if (error) {
+			LogError("avcodec_receive_packet", error);
+			deinitEncoding();
+			return false;
+		}
+
+		pkt->stream_index = stream->index;
+		av_packet_rescale_ts(pkt, codec->time_base, stream->time_base);
+
+		accumulate_max(
+			_resultDuration,
+			PtsToTimeCeil(pkt->pts, stream->time_base));
+
+		error = AvErrorWrap(av_interleaved_write_frame(_format.get(), pkt));
+		if (error) {
+			LogError("av_interleaved_write_frame", error);
+			deinitEncoding();
+			return false;
+		}
+	}
+
+	return true;
+}
+
+RoundVideoRecorder::RoundVideoRecorder(
+	RoundVideoRecorderDescriptor &&descriptor)
+: _descriptor(std::move(descriptor))
+, _preview(std::make_unique<RpWidget>(_descriptor.container))
+, _private() {
+	setup();
+}
+
+RoundVideoRecorder::~RoundVideoRecorder() = default;
+
+Fn<void(Media::Capture::Chunk)> RoundVideoRecorder::audioChunkProcessor() {
+	return [weak = _private.weak()](Media::Capture::Chunk chunk) {
+		weak.with([copy = std::move(chunk)](Private &that) {
+			that.push(copy);
+		});
+	};
+}
+
+void RoundVideoRecorder::hide(Fn<void(RoundVideoResult)> done) {
+	if (done) {
+		_private.with([done = std::move(done)](Private &that) {
+			done(that.finish());
+		});
+	}
+
+	setPaused(true);
+
+	_preview->hide();
+	if (const auto onstack = _descriptor.hidden) {
+		onstack(this);
+	}
+}
+
+void RoundVideoRecorder::setup() {
+	const auto raw = _preview.get();
+
+	const auto side = style::ConvertScale(kSide * 3 / 4);
+	_descriptor.container->sizeValue(
+	) | rpl::start_with_next([=](QSize outer) {
+		raw->setGeometry(
+			style::centerrect(
+				QRect(QPoint(), outer),
+				QRect(0, 0, side, side)));
+	}, raw->lifetime());
+
+	raw->paintRequest() | rpl::start_with_next([=] {
+		auto p = QPainter(raw);
+		auto hq = PainterHighQualityEnabler(p);
+
+		auto info = _descriptor.track->frameWithInfo(true);
+		if (!info.original.isNull()) {
+			const auto owidth = info.original.width();
+			const auto oheight = info.original.height();
+			const auto omin = std::min(owidth, oheight);
+			const auto ox = (owidth > oheight) ? (owidth - oheight) / 2 : 0;
+			const auto oy = (owidth < oheight) ? (oheight - owidth) / 2 : 0;
+			const auto from = QRect(ox, oy, omin, omin);
+			p.drawImage(QRect(0, 0, side, side), info.original, from);
+		} else {
+			p.setPen(Qt::NoPen);
+			p.setBrush(QColor(0, 0, 0));
+			p.drawEllipse(0, 0, side, side);
+		}
+		_descriptor.track->markFrameShown();
+	}, raw->lifetime());
+
+	_descriptor.track->renderNextFrame() | rpl::start_with_next([=] {
+		const auto info = _descriptor.track->frameWithInfo(true);
+		if (!info.original.isNull() && _lastAddedIndex != info.index) {
+			_lastAddedIndex = info.index;
+			const auto ts = info.mcstimestamp;
+			_private.with([copy = info.original, ts](Private &that) {
+				that.push(ts, copy);
+			});
+		}
+		raw->update();
+	}, raw->lifetime());
+
+	raw->show();
+	raw->raise();
+}
+
+void RoundVideoRecorder::setPaused(bool paused) {
+	if (_paused == paused) {
+		return;
+	}
+	_paused = paused;
+	_preview->update();
+}
+
+
+} // namespace Ui
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.h b/Telegram/SourceFiles/ui/controls/round_video_recorder.h
new file mode 100644
index 000000000..1cfb2d289
--- /dev/null
+++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.h
@@ -0,0 +1,65 @@
+/*
+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
+*/
+#pragma once
+
+#include <crl/crl_object_on_queue.h>
+
+namespace Media::Capture {
+struct Chunk;
+} // namespace Media::Capture
+
+namespace tgcalls {
+class VideoCaptureInterface;
+} // namespace tgcalls
+
+namespace Webrtc {
+class VideoTrack;
+} // namespace Webrtc
+
+namespace Ui {
+
+class RpWidget;
+class RoundVideoRecorder;
+
+struct RoundVideoRecorderDescriptor {
+	not_null<RpWidget*> container;
+	Fn<void(not_null<RoundVideoRecorder*>)> hidden;
+	std::shared_ptr<tgcalls::VideoCaptureInterface> capturer;
+	std::shared_ptr<Webrtc::VideoTrack> track;
+};
+
+struct RoundVideoResult {
+	QByteArray content;
+	QByteArray waveform;
+	crl::time duration = 0;
+};
+
+class RoundVideoRecorder final {
+public:
+	explicit RoundVideoRecorder(RoundVideoRecorderDescriptor &&descriptor);
+	~RoundVideoRecorder();
+
+	[[nodiscard]] Fn<void(Media::Capture::Chunk)> audioChunkProcessor();
+
+	void setPaused(bool paused);
+	void hide(Fn<void(RoundVideoResult)> done = nullptr);
+
+private:
+	class Private;
+
+	void setup();
+
+	const RoundVideoRecorderDescriptor _descriptor;
+	std::unique_ptr<RpWidget> _preview;
+	crl::object_on_queue<Private> _private;
+	int _lastAddedIndex = 0;
+	bool _paused = false;
+
+};
+
+} // namespace Ui
diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake
index 7fd873fe6..e42ee37a2 100644
--- a/Telegram/cmake/td_ui.cmake
+++ b/Telegram/cmake/td_ui.cmake
@@ -366,6 +366,8 @@ PRIVATE
     ui/controls/invite_link_label.h
     ui/controls/peer_list_dummy.cpp
     ui/controls/peer_list_dummy.h
+    ui/controls/round_video_recorder.cpp
+    ui/controls/round_video_recorder.h
     ui/controls/send_as_button.cpp
     ui/controls/send_as_button.h
     ui/controls/send_button.cpp
@@ -500,4 +502,6 @@ PRIVATE
     desktop-app::lib_spellcheck
     desktop-app::lib_stripe
     desktop-app::external_kcoreaddons
+    desktop-app::external_openh264
+    desktop-app::external_webrtc
 )
diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc
index 8751e27d5..fc726486e 160000
--- a/Telegram/lib_webrtc
+++ b/Telegram/lib_webrtc
@@ -1 +1 @@
-Subproject commit 8751e27d50d2f26b5d20673e5ddba38e90953570
+Subproject commit fc726486ebd261283583b5cd5f6a97a18b2ab6ca