diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp
index 67191343e..b7c68db7e 100644
--- a/Telegram/SourceFiles/app.cpp
+++ b/Telegram/SourceFiles/app.cpp
@@ -422,14 +422,11 @@ namespace App {
 	HistoryItem *histItemById(ChannelId channelId, MsgId itemId) {
 		if (!itemId) return nullptr;
 
-		auto data = fetchMsgsData(channelId, false);
+		const auto data = fetchMsgsData(channelId, false);
 		if (!data) return nullptr;
 
-		auto i = data->constFind(itemId);
-		if (i != data->cend()) {
-			return i.value();
-		}
-		return nullptr;
+		const auto i = data->constFind(itemId);
+		return (i != data->cend()) ? i.value() : nullptr;
 	}
 
 	HistoryItem *histItemById(const ChannelData *channel, MsgId itemId) {
diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp
index 373155f89..f93c6b3f0 100644
--- a/Telegram/SourceFiles/data/data_document.cpp
+++ b/Telegram/SourceFiles/data/data_document.cpp
@@ -154,7 +154,7 @@ QString FileNameUnsafe(
 			if (QRegularExpression(qsl("^[a-zA-Z_0-9]+$")).match(ext).hasMatch()) {
 				QStringList filters = filter.split(sep);
 				if (filters.size() > 1) {
-					QString first = filters.at(0);
+					const auto &first = filters.at(0);
 					int32 start = first.indexOf(qsl("(*."));
 					if (start >= 0) {
 						if (!QRegularExpression(qsl("\\(\\*\\.") + ext + qsl("[\\)\\s]"), QRegularExpression::CaseInsensitiveOption).match(first).hasMatch()) {
@@ -287,202 +287,6 @@ QString documentSaveFilename(const DocumentData *data, bool forceSavingAs = fals
 	return FileNameForSave(caption, filter, prefix, name, forceSavingAs, dir);
 }
 
-void StartStreaming(
-		not_null<DocumentData*> document,
-		Data::FileOrigin origin) {
-	AssertIsDebug();
-
-	using namespace Media::Streaming;
-	if (auto loader = document->createStreamingLoader(origin)) {
-		static auto player = std::unique_ptr<Player>();
-		static auto pauseOnSeek = false;
-		static auto position = crl::time(0);
-		static auto preloadedAudio = crl::time(0);
-		static auto preloadedVideo = crl::time(0);
-		static auto duration = crl::time(0);
-		static auto options = PlaybackOptions();
-		static auto speed = 1.;
-		static auto step = pow(2., 1. / 12);
-		static auto frame = QImage();
-
-		class Panel
-#if defined Q_OS_MAC && !defined OS_MAC_OLD
-			: public Ui::RpWidgetWrap<QOpenGLWidget> {
-			using Parent = Ui::RpWidgetWrap<QOpenGLWidget>;
-#else // Q_OS_MAC && !OS_MAC_OLD
-			: public Ui::RpWidget {
-			using Parent = Ui::RpWidget;
-#endif // Q_OS_MAC && !OS_MAC_OLD
-
-		public:
-			Panel() : Parent(nullptr) {
-			}
-
-		protected:
-			void paintEvent(QPaintEvent *e) override {
-			}
-			void keyPressEvent(QKeyEvent *e) override {
-				if (e->key() == Qt::Key_Space) {
-					if (player->paused()) {
-						player->resume();
-					} else {
-						player->pause();
-					}
-				} else if (e->key() == Qt::Key_Plus) {
-					speed = std::min(speed * step, 2.);
-					player->setSpeed(speed);
-				} else if (e->key() == Qt::Key_Minus) {
-					speed = std::max(speed / step, 0.5);
-					player->setSpeed(speed);
-				}
-			}
-			void mousePressEvent(QMouseEvent *e) override {
-				pauseOnSeek = player->paused();
-				player->pause();
-			}
-			void mouseReleaseEvent(QMouseEvent *e) override {
-				if (player->ready()) {
-					frame = player->frame({});
-				}
-				preloadedAudio
-					= preloadedVideo
-					= position
-					= options.position
-					= std::clamp(
-						(duration * e->pos().x()) / width(),
-						crl::time(0),
-						crl::time(duration));
-				player->play(options);
-			}
-
-		};
-
-		static auto video = base::unique_qptr<Panel>();
-
-		player = std::make_unique<Player>(
-			&document->owner(),
-			std::move(loader));
-		base::take(video) = nullptr;
-		player->lifetime().add([] {
-			base::take(video) = nullptr;
-		});
-		document->session().lifetime().add([] {
-			base::take(player) = nullptr;
-		});
-
-		options.speed = speed;
-		//options.syncVideoByAudio = false;
-		preloadedAudio = preloadedVideo = position = options.position = 0;
-		frame = QImage();
-		player->play(options);
-		player->updates(
-		) | rpl::start_with_next_error_done([=](Update &&update) {
-			update.data.match([&](Information &update) {
-				duration = std::max(
-					update.video.state.duration,
-					update.audio.state.duration);
-				if (video) {
-					if (update.video.cover.isNull()) {
-						base::take(video) = nullptr;
-					} else {
-						video->update();
-					}
-				} else if (!update.video.cover.isNull()) {
-					video = base::make_unique_q<Panel>();
-					video->setAttribute(Qt::WA_OpaquePaintEvent);
-					video->paintRequest(
-					) | rpl::start_with_next([=](QRect rect) {
-						const auto till1 = duration
-							? (position * video->width() / duration)
-							: 0;
-						const auto till2 = duration
-							? (std::min(preloadedAudio, preloadedVideo)
-								* video->width()
-								/ duration)
-							: 0;
-						if (player->ready()) {
-							Painter(video.get()).drawImage(
-								video->rect(),
-								player->frame({}));
-						} else if (!frame.isNull()) {
-							Painter(video.get()).drawImage(
-								video->rect(),
-								frame);
-						} else {
-							Painter(video.get()).fillRect(
-								rect,
-								Qt::black);
-						}
-						Painter(video.get()).fillRect(
-							0,
-							0,
-							till1,
-							video->height(),
-							QColor(255, 255, 255, 64));
-						if (till2 > till1) {
-							Painter(video.get()).fillRect(
-								till1,
-								0,
-								till2 - till1,
-								video->height(),
-								QColor(255, 255, 255, 32));
-						}
-					}, video->lifetime());
-					const auto size = QSize(
-						ConvertScale(update.video.size.width()),
-						ConvertScale(update.video.size.height()));
-					const auto center = App::wnd()->geometry().center();
-					video->setGeometry(QRect(
-						center - QPoint(size.width(), size.height()) / 2,
-						size));
-					video->show();
-					video->shownValue(
-					) | rpl::start_with_next([=](bool shown) {
-						if (!shown) {
-							base::take(player) = nullptr;
-						}
-					}, video->lifetime());
-				}
-			}, [&](PreloadedVideo &update) {
-				if (preloadedVideo < update.till) {
-					if (preloadedVideo < preloadedAudio) {
-						video->update();
-					}
-					preloadedVideo = update.till;
-				}
-			}, [&](UpdateVideo &update) {
-				Expects(video != nullptr);
-
-				if (position < update.position) {
-					position = update.position;
-				}
-				video->update();
-			}, [&](PreloadedAudio &update) {
-				if (preloadedAudio < update.till) {
-					if (video && preloadedAudio < preloadedVideo) {
-						video->update();
-					}
-					preloadedAudio = update.till;
-				}
-			}, [&](UpdateAudio &update) {
-				if (position < update.position) {
-					position = update.position;
-					if (video) {
-						video->update();
-					}
-				}
-			}, [&](WaitingForData) {
-			}, [&](MutedByOther) {
-			}, [&](Finished) {
-				base::take(player) = nullptr;
-			});
-		}, [=](const Error &error) {
-			base::take(video) = nullptr;
-		}, [=] {
-		}, player->lifetime());
-	}
-}
-
 void DocumentOpenClickHandler::Open(
 		Data::FileOrigin origin,
 		not_null<DocumentData*> data,
@@ -503,8 +307,8 @@ void DocumentOpenClickHandler::Open(
 			return;
 		}
 	}
-	if (data->isAudioFile() || data->isVideoFile()) {
-		StartStreaming(data, origin);
+	if (data->canBePlayed()) {
+		Core::App().showDocument(data, context);
 		return;
 	}
 	if (!location.isEmpty() || (!data->data().isEmpty() && (playVoice || playMusic || playVideo || playAnimation))) {
@@ -536,25 +340,6 @@ void DocumentOpenClickHandler::Open(
 				Media::Player::mixer()->play(song);
 				Media::Player::Updated().notify(song);
 			}
-		} else if (playVideo) {
-			if (!data->data().isEmpty()) {
-				Core::App().showDocument(data, context);
-			} else if (location.accessEnable()) {
-				Core::App().showDocument(data, context);
-				location.accessDisable();
-			} else {
-				const auto filepath = location.name();
-				if (Data::IsValidMediaFile(filepath)) {
-					File::Launch(filepath);
-				}
-			}
-			data->owner().markMediaRead(data);
-		} else if (data->isVoiceMessage() || data->isAudioFile() || data->isVideoFile()) {
-			const auto filepath = location.name();
-			if (Data::IsValidMediaFile(filepath)) {
-				File::Launch(filepath);
-			}
-			data->owner().markMediaRead(data);
 		} else if (data->size < App::kImageSizeLimit) {
 			if (!data->data().isEmpty() && playAnimation) {
 				if (action == ActionOnLoadPlayInline && context) {
@@ -610,14 +395,10 @@ void GifOpenClickHandler::onClickImpl() const {
 void DocumentSaveClickHandler::Save(
 		Data::FileOrigin origin,
 		not_null<DocumentData*> data,
+		HistoryItem *context,
 		bool forceSavingAs) {
 	if (!data->date) return;
 
-	if (data->isAudioFile() || data->isVideoFile()) {
-		StartStreaming(data, origin);
-		return;
-	}
-
 	auto filepath = data->filepath(
 		DocumentData::FilePathResolveSaveFromDataSilent,
 		forceSavingAs);
@@ -635,7 +416,7 @@ void DocumentSaveClickHandler::Save(
 }
 
 void DocumentSaveClickHandler::onClickImpl() const {
-	Save(context(), document());
+	Save(context(), document(), getActionItem());
 }
 
 void DocumentCancelClickHandler::onClickImpl() const {
@@ -683,51 +464,44 @@ AuthSession &DocumentData::session() const {
 	return _owner->session();
 }
 
-void DocumentData::setattributes(const QVector<MTPDocumentAttribute> &attributes) {
+void DocumentData::setattributes(
+		const QVector<MTPDocumentAttribute> &attributes) {
 	_isImage = false;
 	_supportsStreaming = false;
-	for (int32 i = 0, l = attributes.size(); i < l; ++i) {
-		switch (attributes[i].type()) {
-		case mtpc_documentAttributeImageSize: {
-			auto &d = attributes[i].c_documentAttributeImageSize();
-			dimensions = QSize(d.vw.v, d.vh.v);
-		} break;
-		case mtpc_documentAttributeAnimated:
+	for (const auto &attribute : attributes) {
+		attribute.match([&](const MTPDdocumentAttributeImageSize & data) {
+			dimensions = QSize(data.vw.v, data.vh.v);
+		}, [&](const MTPDdocumentAttributeAnimated & data) {
 			if (type == FileDocument
 				|| type == StickerDocument
 				|| type == VideoDocument) {
 				type = AnimatedDocument;
 				_additional = nullptr;
-			} break;
-		case mtpc_documentAttributeSticker: {
-			auto &d = attributes[i].c_documentAttributeSticker();
+			}
+		}, [&](const MTPDdocumentAttributeSticker & data) {
 			if (type == FileDocument) {
 				type = StickerDocument;
 				_additional = std::make_unique<StickerData>();
 			}
 			if (sticker()) {
-				sticker()->alt = qs(d.valt);
+				sticker()->alt = qs(data.valt);
 				if (sticker()->set.type() != mtpc_inputStickerSetID
-					|| d.vstickerset.type() == mtpc_inputStickerSetID) {
-					sticker()->set = d.vstickerset;
+					|| data.vstickerset.type() == mtpc_inputStickerSetID) {
+					sticker()->set = data.vstickerset;
 				}
 			}
-		} break;
-		case mtpc_documentAttributeVideo: {
-			auto &d = attributes[i].c_documentAttributeVideo();
+		}, [&](const MTPDdocumentAttributeVideo & data) {
 			if (type == FileDocument) {
-				type = d.is_round_message()
+				type = data.is_round_message()
 					? RoundVideoDocument
 					: VideoDocument;
 			}
-			_duration = d.vduration.v;
-			_supportsStreaming = d.is_supports_streaming();
-			dimensions = QSize(d.vw.v, d.vh.v);
-		} break;
-		case mtpc_documentAttributeAudio: {
-			auto &d = attributes[i].c_documentAttributeAudio();
+			_duration = data.vduration.v;
+			_supportsStreaming = data.is_supports_streaming();
+			dimensions = QSize(data.vw.v, data.vh.v);
+		}, [&](const MTPDdocumentAttributeAudio & data) {
 			if (type == FileDocument) {
-				if (d.is_voice()) {
+				if (data.is_voice()) {
 					type = VoiceDocument;
 					_additional = std::make_unique<VoiceData>();
 				} else {
@@ -736,25 +510,19 @@ void DocumentData::setattributes(const QVector<MTPDocumentAttribute> &attributes
 				}
 			}
 			if (const auto voiceData = voice()) {
-				voiceData->duration = d.vduration.v;
-				VoiceWaveform waveform = documentWaveformDecode(qba(d.vwaveform));
-				uchar wavemax = 0;
-				for (int32 i = 0, l = waveform.size(); i < l; ++i) {
-					uchar waveat = waveform.at(i);
-					if (wavemax < waveat) wavemax = waveat;
-				}
-				voiceData->waveform = waveform;
-				voiceData->wavemax = wavemax;
+				voiceData->duration = data.vduration.v;
+				voiceData->waveform = documentWaveformDecode(
+					qba(data.vwaveform));
+				voiceData->wavemax = voiceData->waveform.empty()
+					? uchar(0)
+					: *ranges::max_element(voiceData->waveform);
 			} else if (const auto songData = song()) {
-				songData->duration = d.vduration.v;
-				songData->title = qs(d.vtitle);
-				songData->performer = qs(d.vperformer);
+				songData->duration = data.vduration.v;
+				songData->title = qs(data.vtitle);
+				songData->performer = qs(data.vperformer);
 			}
-		} break;
-		case mtpc_documentAttributeFilename: {
-			const auto &attribute = attributes[i];
-			_filename = qs(
-				attribute.c_documentAttributeFilename().vfile_name);
+		}, [&](const MTPDdocumentAttributeFilename & data) {
+			_filename = qs(data.vfile_name);
 
 			// We don't want LTR/RTL mark/embedding/override/isolate chars
 			// in filenames, because they introduce a security issue, when
@@ -772,8 +540,8 @@ void DocumentData::setattributes(const QVector<MTPDocumentAttribute> &attributes
 			for (const auto ch : controls) {
 				_filename = std::move(_filename).replace(ch, "_");
 			}
-		} break;
-		}
+		}, [&](const MTPDdocumentAttributeHasStickers &data) {
+		});
 	}
 	if (type == StickerDocument) {
 		if (dimensions.width() <= 0
@@ -1495,8 +1263,27 @@ bool DocumentData::hasRemoteLocation() const {
 	return (_dc != 0 && _access != 0);
 }
 
+bool DocumentData::canBeStreamed() const {
+	return hasRemoteLocation()
+		&& (isAudioFile()
+			|| ((isAnimation() || isVideoFile()) && supportsStreaming()));
+}
+
+bool DocumentData::canBePlayed() const {
+	return (isAnimation() || isVideoFile() || isAudioFile())
+		&& (loaded() || canBeStreamed());
+}
+
 auto DocumentData::createStreamingLoader(Data::FileOrigin origin) const
 -> std::unique_ptr<Media::Streaming::Loader> {
+	// #TODO streaming create local file loader
+	//auto &location = this->location(true);
+	//if (!_doc->data().isEmpty()) {
+	//	initStreaming();
+	//} else if (location.accessEnable()) {
+	//	initStreaming();
+	//	location.accessDisable();
+	//}
 	return hasRemoteLocation()
 		? std::make_unique<Media::Streaming::LoaderMtproto>(
 			&session().api(),
diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h
index 215fa934d..e04ebe5d7 100644
--- a/Telegram/SourceFiles/data/data_document.h
+++ b/Telegram/SourceFiles/data/data_document.h
@@ -223,6 +223,8 @@ public:
 		const QString &songPerformer);
 	[[nodiscard]] QString composeNameString() const;
 
+	[[nodiscard]] bool canBePlayed() const;
+	[[nodiscard]] bool canBeStreamed() const;
 	[[nodiscard]] auto createStreamingLoader(Data::FileOrigin origin) const
 		-> std::unique_ptr<Media::Streaming::Loader>;
 
@@ -303,6 +305,7 @@ public:
 	static void Save(
 		Data::FileOrigin origin,
 		not_null<DocumentData*> document,
+		HistoryItem *context,
 		bool forceSavingAs = false);
 
 protected:
diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
index 8e62fde75..c6f99ef25 100644
--- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
+++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
@@ -1092,7 +1092,11 @@ void InnerWidget::savePhotoToFile(PhotoData *photo) {
 }
 
 void InnerWidget::saveDocumentToFile(DocumentData *document) {
-	DocumentSaveClickHandler::Save(Data::FileOrigin(), document, true);
+	DocumentSaveClickHandler::Save(
+		Data::FileOrigin(),
+		document,
+		nullptr,
+		true);
 }
 
 void InnerWidget::copyContextImage(PhotoData *photo) {
diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index b14bc5ea1..9e23d944a 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -1823,7 +1823,11 @@ void HistoryInner::showContextInFolder(not_null<DocumentData*> document) {
 void HistoryInner::saveDocumentToFile(
 		FullMsgId contextId,
 		not_null<DocumentData*> document) {
-	DocumentSaveClickHandler::Save(contextId, document, true);
+	DocumentSaveClickHandler::Save(
+		contextId,
+		document,
+		App::histItemById(contextId),
+		true);
 }
 
 void HistoryInner::openContextGif(FullMsgId itemId) {
diff --git a/Telegram/SourceFiles/history/media/history_media_document.cpp b/Telegram/SourceFiles/history/media/history_media_document.cpp
index 68d0c5a5e..af318505e 100644
--- a/Telegram/SourceFiles/history/media/history_media_document.cpp
+++ b/Telegram/SourceFiles/history/media/history_media_document.cpp
@@ -621,7 +621,6 @@ bool HistoryDocument::updateStatusText() const {
 	} else if (_data->loading()) {
 		statusSize = _data->loadOffset();
 	} else if (_data->loaded()) {
-		using State = Media::Player::State;
 		statusSize = FileStatusSizeLoaded;
 		if (_data->isVoiceMessage()) {
 			auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Voice);
diff --git a/Telegram/SourceFiles/history/media/history_media_gif.cpp b/Telegram/SourceFiles/history/media/history_media_gif.cpp
index 0ca34ffbb..3e4612c18 100644
--- a/Telegram/SourceFiles/history/media/history_media_gif.cpp
+++ b/Telegram/SourceFiles/history/media/history_media_gif.cpp
@@ -13,7 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/audio/media_audio.h"
 #include "media/clip/media_clip_reader.h"
 #include "media/player/media_player_round_controller.h"
-#include "media/view/media_clip_playback.h"
+#include "media/view/media_view_playback_progress.h"
 #include "boxes/confirm_box.h"
 #include "history/history_item_components.h"
 #include "history/history_item.h"
@@ -833,7 +833,7 @@ Media::Clip::Reader *HistoryGif::currentReader() const {
 	return (_gif && _gif->ready()) ? _gif.get() : nullptr;
 }
 
-Media::Clip::Playback *HistoryGif::videoPlayback() const {
+Media::View::PlaybackProgress *HistoryGif::videoPlayback() const {
 	if (const auto video = activeRoundVideo()) {
 		return video->playback();
 	}
diff --git a/Telegram/SourceFiles/history/media/history_media_gif.h b/Telegram/SourceFiles/history/media/history_media_gif.h
index 621e91c4d..891ab8c0e 100644
--- a/Telegram/SourceFiles/history/media/history_media_gif.h
+++ b/Telegram/SourceFiles/history/media/history_media_gif.h
@@ -14,9 +14,9 @@ struct HistoryMessageReply;
 struct HistoryMessageForwarded;
 
 namespace Media {
-namespace Clip {
-class Playback;
-} // namespace Clip
+namespace View {
+class PlaybackProgress;
+} // namespace View
 
 namespace Player {
 class RoundController;
@@ -91,7 +91,7 @@ private:
 	Media::Player::RoundController *activeRoundVideo() const;
 	Media::Clip::Reader *activeRoundPlayer() const;
 	Media::Clip::Reader *currentReader() const;
-	Media::Clip::Playback *videoPlayback() const;
+	Media::View::PlaybackProgress *videoPlayback() const;
 	void clipCallback(Media::Clip::Notification notification);
 
 	bool needInfoDisplay() const;
diff --git a/Telegram/SourceFiles/history/media/history_media_video.cpp b/Telegram/SourceFiles/history/media/history_media_video.cpp
index 1e5a5fd38..af5a82f54 100644
--- a/Telegram/SourceFiles/history/media/history_media_video.cpp
+++ b/Telegram/SourceFiles/history/media/history_media_video.cpp
@@ -226,8 +226,9 @@ void HistoryVideo::draw(Painter &p, const QRect &r, TextSelection selection, crl
 		p.setOpacity(1);
 	}
 
-	auto icon = ([this, radial, selected, loaded]() -> const style::icon * {
-		if (loaded && !radial) {
+	const auto canPlay = _data->canBePlayed();
+	auto icon = [&]() -> const style::icon * {
+		if (canPlay && !radial) {
 			return &(selected ? st::historyFileThumbPlaySelected : st::historyFileThumbPlay);
 		} else if (radial || _data->loading()) {
 			if (_parent->data()->id > 0 || _data->uploading()) {
@@ -236,7 +237,7 @@ void HistoryVideo::draw(Painter &p, const QRect &r, TextSelection selection, crl
 			return nullptr;
 		}
 		return &(selected ? st::historyFileThumbDownloadSelected : st::historyFileThumbDownload);
-	})();
+	}();
 	if (icon) {
 		icon->paintInCenter(p, inner);
 	}
@@ -275,7 +276,7 @@ TextState HistoryVideo::textState(QPoint point, StateRequest request) const {
 	}
 
 	auto result = TextState(_parent);
-	bool loaded = _data->loaded();
+	const auto canPlay = _data->canBePlayed();
 
 	auto paintx = 0, painty = 0, paintw = width(), painth = height();
 	bool bubble = _parent->hasBubble();
@@ -300,7 +301,7 @@ TextState HistoryVideo::textState(QPoint point, StateRequest request) const {
 		if (_data->uploading()) {
 			result.link = _cancell;
 		} else {
-			result.link = loaded ? _openl : (_data->loading() ? _cancell : _savel);
+			result.link = canPlay ? _openl : (_data->loading() ? _cancell : _savel);
 		}
 	}
 	if (_caption.isEmpty() && _parent->media() == this) {
@@ -389,10 +390,11 @@ void HistoryVideo::drawGrouped(
 		p.drawEllipse(inner);
 	}
 
+	const auto canPlay = _data->canBePlayed();
 	auto icon = [&]() -> const style::icon * {
 		if (_data->waitingForAlbum()) {
 			return &(selected ? st::historyFileThumbWaitingSelected : st::historyFileThumbWaiting);
-		} else if (loaded && !radial) {
+		} else if (canPlay && !radial) {
 			return &(selected ? st::historyFileThumbPlaySelected : st::historyFileThumbPlay);
 		} else if (radial || _data->loading()) {
 			if (_parent->data()->id > 0 || _data->uploading()) {
@@ -436,7 +438,7 @@ TextState HistoryVideo::getStateGrouped(
 	}
 	return TextState(_parent, _data->uploading()
 		? _cancell
-		: _data->loaded()
+		: _data->canBePlayed()
 		? _openl
 		: _data->loading()
 		? _cancell
diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
index d09d1dcc1..4c17541d2 100644
--- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
@@ -122,6 +122,9 @@ void AddSaveDocumentAction(
 		not_null<Ui::PopupMenu*> menu,
 		Data::FileOrigin origin,
 		not_null<DocumentData*> document) {
+	const auto save = [=] {
+		DocumentSaveClickHandler::Save(origin, document, nullptr, true);
+	};
 	menu->addAction(
 		lang(document->isVideoFile()
 			? lng_context_save_video
@@ -135,7 +138,7 @@ void AddSaveDocumentAction(
 		App::LambdaDelayed(
 			st::defaultDropdownMenu.menu.ripple.hideDuration,
 			&Auth(),
-			[=] { DocumentSaveClickHandler::Save(origin, document, true); }));
+			save));
 }
 
 void AddDocumentActions(
diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
index 61b7d762a..d80c8e634 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
@@ -1277,6 +1277,7 @@ void ListWidget::showContextMenu(
 							DocumentSaveClickHandler::Save(
 								itemFullId,
 								document,
+								App::histItemById(itemFullId),
 								true);
 						});
 					_contextMenu->addAction(
diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp
index f881e26c1..3983afb8c 100644
--- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp
+++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp
@@ -894,7 +894,6 @@ bool File::updateStatusText() const {
 	} else if (_document->loading()) {
 		statusSize = _document->loadOffset();
 	} else if (_document->loaded()) {
-		using State = Media::Player::State;
 		if (_document->isVoiceMessage()) {
 			statusSize = FileStatusSizeLoaded;
 			auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Voice);
diff --git a/Telegram/SourceFiles/media/audio/media_audio.cpp b/Telegram/SourceFiles/media/audio/media_audio.cpp
index 0683a5995..cd9d6fc9a 100644
--- a/Telegram/SourceFiles/media/audio/media_audio.cpp
+++ b/Telegram/SourceFiles/media/audio/media_audio.cpp
@@ -729,10 +729,10 @@ void Mixer::resetFadeStartPosition(AudioMsgId::Type type, int positionInBuffered
 				return;
 			}
 
-			const auto stoppedAtEnd = (alState == AL_STOPPED)
-				&& (!IsStopped(track->state.state)
-					|| IsStoppedAtEnd(track->state.state))
-				|| track->state.waitingForData;
+			const auto stoppedAtEnd = track->state.waitingForData
+				|| ((alState == AL_STOPPED)
+					&& (!IsStopped(track->state.state)
+						|| IsStoppedAtEnd(track->state.state)));
 			positionInBuffered = stoppedAtEnd
 				? track->bufferedLength
 				: alSampleOffset;
@@ -1438,10 +1438,10 @@ int32 Fader::updateOnePlayback(Mixer::Track *track, bool &hasPlaying, bool &hasF
 	}
 
 	int32 emitSignals = 0;
-	const auto stoppedAtEnd = (alState == AL_STOPPED)
-		&& (!IsStopped(track->state.state)
-			|| IsStoppedAtEnd(track->state.state))
-		|| track->state.waitingForData;
+	const auto stoppedAtEnd = track->state.waitingForData
+		|| ((alState == AL_STOPPED)
+			&& (!IsStopped(track->state.state)
+				|| IsStoppedAtEnd(track->state.state)));
 	const auto positionInBuffered = stoppedAtEnd
 		? track->bufferedLength
 		: alSampleOffset;
diff --git a/Telegram/SourceFiles/media/player/media_player_button.h b/Telegram/SourceFiles/media/player/media_player_button.h
index 7c33af2d7..6ad124e8c 100644
--- a/Telegram/SourceFiles/media/player/media_player_button.h
+++ b/Telegram/SourceFiles/media/player/media_player_button.h
@@ -47,5 +47,5 @@ private:
 
 };
 
-} // namespace Clip
+} // namespace Player
 } // namespace Media
diff --git a/Telegram/SourceFiles/media/player/media_player_cover.cpp b/Telegram/SourceFiles/media/player/media_player_cover.cpp
index f84156aee..cdd3e6679 100644
--- a/Telegram/SourceFiles/media/player/media_player_cover.cpp
+++ b/Telegram/SourceFiles/media/player/media_player_cover.cpp
@@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/widgets/continuous_sliders.h"
 #include "ui/widgets/buttons.h"
 #include "media/audio/media_audio.h"
-#include "media/view/media_clip_playback.h"
+#include "media/view/media_view_playback_progress.h"
 #include "media/player/media_player_button.h"
 #include "media/player/media_player_instance.h"
 #include "media/player/media_player_volume_controller.h"
@@ -62,7 +62,7 @@ CoverWidget::CoverWidget(QWidget *parent) : RpWidget(parent)
 , _timeLabel(this, st::mediaPlayerTime)
 , _close(this, st::mediaPlayerPanelClose)
 , _playbackSlider(this, st::mediaPlayerPanelPlayback)
-, _playback(std::make_unique<Clip::Playback>())
+, _playbackProgress(std::make_unique<View::PlaybackProgress>())
 , _playPause(this)
 , _volumeToggle(this, st::mediaPlayerVolumeToggle)
 , _volumeController(this)
@@ -76,18 +76,18 @@ CoverWidget::CoverWidget(QWidget *parent) : RpWidget(parent)
 	_timeLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
 	setMouseTracking(true);
 
-	_playback->setInLoadingStateChangedCallback([=](bool loading) {
+	_playbackProgress->setInLoadingStateChangedCallback([=](bool loading) {
 		_playbackSlider->setDisabled(loading);
 	});
-	_playback->setValueChangedCallback([=](float64 value) {
+	_playbackProgress->setValueChangedCallback([=](float64 value) {
 		_playbackSlider->setValue(value);
 	});
 	_playbackSlider->setChangeProgressCallback([=](float64 value) {
-		_playback->setValue(value, false);
+		_playbackProgress->setValue(value, false);
 		handleSeekProgress(value);
 	});
 	_playbackSlider->setChangeFinishedCallback([=](float64 value) {
-		_playback->setValue(value, false);
+		_playbackProgress->setValue(value, false);
 		handleSeekFinished(value);
 	});
 	_playPause->setClickedCallback([=] {
@@ -240,9 +240,9 @@ void CoverWidget::handleSongUpdate(const TrackState &state) {
 	}
 
 	if (state.id.audio()->loading()) {
-		_playback->updateLoadingState(state.id.audio()->progress());
+		_playbackProgress->updateLoadingState(state.id.audio()->progress());
 	} else {
-		_playback->updateState(state);
+		_playbackProgress->updateState(state);
 	}
 
 	auto stopped = IsStoppedOrStopping(state.state);
diff --git a/Telegram/SourceFiles/media/player/media_player_cover.h b/Telegram/SourceFiles/media/player/media_player_cover.h
index 57ba4e5c6..6cfded681 100644
--- a/Telegram/SourceFiles/media/player/media_player_cover.h
+++ b/Telegram/SourceFiles/media/player/media_player_cover.h
@@ -19,9 +19,9 @@ class MediaSlider;
 } // namespace Ui
 
 namespace Media {
-namespace Clip {
-class Playback;
-} // namespace Clip
+namespace View {
+class PlaybackProgress;
+} // namespace View
 
 namespace Player {
 
@@ -71,7 +71,7 @@ private:
 	object_ptr<Ui::LabelSimple> _timeLabel;
 	object_ptr<Ui::IconButton> _close;
 	object_ptr<Ui::MediaSlider> _playbackSlider;
-	std::unique_ptr<Clip::Playback> _playback;
+	std::unique_ptr<View::PlaybackProgress> _playbackProgress;
 	object_ptr<Ui::IconButton> _previousTrack = { nullptr };
 	object_ptr<PlayButton> _playPause;
 	object_ptr<Ui::IconButton> _nextTrack = { nullptr };
@@ -82,5 +82,5 @@ private:
 
 };
 
-} // namespace Clip
+} // namespace Player
 } // namespace Media
diff --git a/Telegram/SourceFiles/media/player/media_player_float.cpp b/Telegram/SourceFiles/media/player/media_player_float.cpp
index e254f478b..8b0d57bec 100644
--- a/Telegram/SourceFiles/media/player/media_player_float.cpp
+++ b/Telegram/SourceFiles/media/player/media_player_float.cpp
@@ -16,7 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/view/history_view_element.h"
 #include "media/audio/media_audio.h"
 #include "media/clip/media_clip_reader.h"
-#include "media/view/media_clip_playback.h"
+#include "media/view/media_view_playback_progress.h"
 #include "media/player/media_player_instance.h"
 #include "media/player/media_player_round_controller.h"
 #include "window/window_controller.h"
@@ -210,7 +210,7 @@ Clip::Reader *Float::getReader() const {
 	return nullptr;
 }
 
-Clip::Playback *Float::getPlayback() const {
+View::PlaybackProgress *Float::getPlayback() const {
 	if (detached()) {
 		return nullptr;
 	}
diff --git a/Telegram/SourceFiles/media/player/media_player_float.h b/Telegram/SourceFiles/media/player/media_player_float.h
index f845e9685..8637bd510 100644
--- a/Telegram/SourceFiles/media/player/media_player_float.h
+++ b/Telegram/SourceFiles/media/player/media_player_float.h
@@ -16,9 +16,9 @@ enum class Column;
 } // namespace Window
 
 namespace Media {
-namespace Clip {
-class Playback;
-} // namespace Clip
+namespace View {
+class PlaybackProgress;
+} // namespace View
 
 namespace Player {
 
@@ -70,7 +70,7 @@ protected:
 private:
 	float64 outRatio() const;
 	Clip::Reader *getReader() const;
-	Clip::Playback *getPlayback() const;
+	View::PlaybackProgress *getPlayback() const;
 	void repaintItem();
 	void prepareShadow();
 	bool hasFrame() const;
diff --git a/Telegram/SourceFiles/media/player/media_player_instance.h b/Telegram/SourceFiles/media/player/media_player_instance.h
index e20197833..55150f210 100644
--- a/Telegram/SourceFiles/media/player/media_player_instance.h
+++ b/Telegram/SourceFiles/media/player/media_player_instance.h
@@ -212,5 +212,5 @@ private:
 
 };
 
-} // namespace Clip
+} // namespace Player
 } // namespace Media
diff --git a/Telegram/SourceFiles/media/player/media_player_panel.h b/Telegram/SourceFiles/media/player/media_player_panel.h
index 7dc616283..9480da90e 100644
--- a/Telegram/SourceFiles/media/player/media_player_panel.h
+++ b/Telegram/SourceFiles/media/player/media_player_panel.h
@@ -118,5 +118,5 @@ private:
 
 };
 
-} // namespace Clip
+} // namespace Player
 } // namespace Media
diff --git a/Telegram/SourceFiles/media/player/media_player_round_controller.cpp b/Telegram/SourceFiles/media/player/media_player_round_controller.cpp
index 14447732f..8549c8edd 100644
--- a/Telegram/SourceFiles/media/player/media_player_round_controller.cpp
+++ b/Telegram/SourceFiles/media/player/media_player_round_controller.cpp
@@ -10,7 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/audio/media_audio.h"
 #include "media/clip/media_clip_reader.h"
 #include "media/player/media_player_instance.h"
-#include "media/view/media_clip_playback.h"
+#include "media/view/media_view_playback_progress.h"
 #include "history/history_item.h"
 #include "window/window_controller.h"
 #include "data/data_media_types.h"
@@ -56,8 +56,8 @@ RoundController::RoundController(
 		_context->fullId(),
 		[=](Clip::Notification notification) { callback(notification); },
 		Clip::Reader::Mode::Video);
-	_playback = std::make_unique<Clip::Playback>();
-	_playback->setValueChangedCallback([=](float64 value) {
+	_playbackProgress = std::make_unique<View::PlaybackProgress>();
+	_playbackProgress->setValueChangedCallback([=](float64 value) {
 		Auth().data().requestItemRepaint(_context);
 	});
 	Auth().data().markMediaRead(_data);
@@ -95,8 +95,8 @@ Clip::Reader *RoundController::reader() const {
 	return _reader ? _reader.get() : nullptr;
 }
 
-Clip::Playback *RoundController::playback() const {
-	return _playback.get();
+View::PlaybackProgress *RoundController::playback() const {
+	return _playbackProgress.get();
 }
 
 void RoundController::handleAudioUpdate(const TrackState &state) {
@@ -112,8 +112,8 @@ void RoundController::handleAudioUpdate(const TrackState &state) {
 	} else if (another) {
 		return;
 	}
-	if (_playback) {
-		_playback->updateState(state);
+	if (_playbackProgress) {
+		_playbackProgress->updateState(state);
 	}
 	if (IsPaused(state.state) || state.state == State::Pausing) {
 		if (!_reader->videoPaused()) {
diff --git a/Telegram/SourceFiles/media/player/media_player_round_controller.h b/Telegram/SourceFiles/media/player/media_player_round_controller.h
index 9328f43ae..3a0ebd860 100644
--- a/Telegram/SourceFiles/media/player/media_player_round_controller.h
+++ b/Telegram/SourceFiles/media/player/media_player_round_controller.h
@@ -15,9 +15,9 @@ class Controller;
 } // namespace Window
 
 namespace Media {
-namespace Clip {
-class Playback;
-} // namespace Clip
+namespace View {
+class PlaybackProgress;
+} // namespace View
 } // namespace Media
 
 namespace Media {
@@ -39,7 +39,7 @@ public:
 	FullMsgId contextId() const;
 	void pauseResume();
 	Clip::Reader *reader() const;
-	Clip::Playback *playback() const;
+	View::PlaybackProgress *playback() const;
 
 	rpl::lifetime &lifetime();
 
@@ -59,7 +59,7 @@ private:
 	not_null<DocumentData*> _data;
 	not_null<HistoryItem*> _context;
 	Clip::ReaderPointer _reader;
-	std::unique_ptr<Clip::Playback> _playback;
+	std::unique_ptr<View::PlaybackProgress> _playbackProgress;
 
 	rpl::lifetime _lifetime;
 
diff --git a/Telegram/SourceFiles/media/player/media_player_volume_controller.h b/Telegram/SourceFiles/media/player/media_player_volume_controller.h
index c0b4b5e1d..f5e54285a 100644
--- a/Telegram/SourceFiles/media/player/media_player_volume_controller.h
+++ b/Telegram/SourceFiles/media/player/media_player_volume_controller.h
@@ -74,5 +74,5 @@ private:
 
 };
 
-} // namespace Clip
+} // namespace Player
 } // namespace Media
diff --git a/Telegram/SourceFiles/media/player/media_player_widget.cpp b/Telegram/SourceFiles/media/player/media_player_widget.cpp
index d94732481..4bde4e4f0 100644
--- a/Telegram/SourceFiles/media/player/media_player_widget.cpp
+++ b/Telegram/SourceFiles/media/player/media_player_widget.cpp
@@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/effects/ripple_animation.h"
 #include "lang/lang_keys.h"
 #include "media/audio/media_audio.h"
-#include "media/view/media_clip_playback.h"
+#include "media/view/media_view_playback_progress.h"
 #include "media/player/media_player_button.h"
 #include "media/player/media_player_instance.h"
 #include "media/player/media_player_volume_controller.h"
@@ -86,7 +86,7 @@ Widget::Widget(QWidget *parent) : RpWidget(parent)
 , _close(this, st::mediaPlayerClose)
 , _shadow(this)
 , _playbackSlider(this, st::mediaPlayerPlayback)
-, _playback(std::make_unique<Clip::Playback>()) {
+, _playbackProgress(std::make_unique<View::PlaybackProgress>()) {
 	setAttribute(Qt::WA_OpaquePaintEvent);
 	setMouseTracking(true);
 	resize(width(), st::mediaPlayerHeight + st::lineWidth);
@@ -94,24 +94,24 @@ Widget::Widget(QWidget *parent) : RpWidget(parent)
 	_nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
 	_timeLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
 
-	_playback->setInLoadingStateChangedCallback([this](bool loading) {
+	_playbackProgress->setInLoadingStateChangedCallback([this](bool loading) {
 		_playbackSlider->setDisabled(loading);
 	});
-	_playback->setValueChangedCallback([this](float64 value) {
+	_playbackProgress->setValueChangedCallback([this](float64 value) {
 		_playbackSlider->setValue(value);
 	});
 	_playbackSlider->setChangeProgressCallback([this](float64 value) {
 		if (_type != AudioMsgId::Type::Song) {
 			return; // Round video seek is not supported for now :(
 		}
-		_playback->setValue(value, false);
+		_playbackProgress->setValue(value, false);
 		handleSeekProgress(value);
 	});
 	_playbackSlider->setChangeFinishedCallback([this](float64 value) {
 		if (_type != AudioMsgId::Type::Song) {
 			return; // Round video seek is not supported for now :(
 		}
-		_playback->setValue(value, false);
+		_playbackProgress->setValue(value, false);
 		handleSeekFinished(value);
 	});
 	_playPause->setClickedCallback([this] {
@@ -430,9 +430,9 @@ void Widget::handleSongUpdate(const TrackState &state) {
 	}
 
 	if (state.id.audio()->loading()) {
-		_playback->updateLoadingState(state.id.audio()->progress());
+		_playbackProgress->updateLoadingState(state.id.audio()->progress());
 	} else {
-		_playback->updateState(state);
+		_playbackProgress->updateState(state);
 	}
 
 	auto stopped = IsStoppedOrStopping(state.state);
diff --git a/Telegram/SourceFiles/media/player/media_player_widget.h b/Telegram/SourceFiles/media/player/media_player_widget.h
index 5804f24f6..b3009c44e 100644
--- a/Telegram/SourceFiles/media/player/media_player_widget.h
+++ b/Telegram/SourceFiles/media/player/media_player_widget.h
@@ -20,10 +20,12 @@ class FilledSlider;
 } // namespace Ui
 
 namespace Media {
-namespace Clip {
-class Playback;
+namespace View {
+class PlaybackProgress;
 } // namespace Clip
+} // namespace Media
 
+namespace Media {
 namespace Player {
 
 class PlayButton;
@@ -109,11 +111,11 @@ private:
 	object_ptr<Ui::IconButton> _close;
 	object_ptr<Ui::PlainShadow> _shadow = { nullptr };
 	object_ptr<Ui::FilledSlider> _playbackSlider;
-	std::unique_ptr<Clip::Playback> _playback;
+	std::unique_ptr<View::PlaybackProgress> _playbackProgress;
 
 	rpl::lifetime _playlistChangesLifetime;
 
 };
 
-} // namespace Clip
+} // namespace Player
 } // namespace Media
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_common.h b/Telegram/SourceFiles/media/streaming/media_streaming_common.h
index 8e4040b65..a00f50a42 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_common.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_common.h
@@ -36,6 +36,7 @@ struct PlaybackOptions {
 	crl::time position = 0;
 	float64 speed = 1.; // Valid values between 0.5 and 2.
 	bool syncVideoByAudio = true;
+	bool dropStaleFrames = true;
 };
 
 struct TrackState {
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
index 493d7507a..259f479fd 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
@@ -513,7 +513,7 @@ bool Player::failed() const {
 }
 
 bool Player::playing() const {
-	return (_stage == Stage::Started) && !_paused;
+	return (_stage == Stage::Started) && !_paused && !finished();
 }
 
 bool Player::buffering() const {
@@ -524,6 +524,12 @@ bool Player::paused() const {
 	return _pausedByUser;
 }
 
+bool Player::finished() const {
+	return (_stage == Stage::Started)
+		&& (!_audio || _audioFinished)
+		&& (!_video || _videoFinished);
+}
+
 void Player::setSpeed(float64 speed) {
 	Expects(valid());
 	Expects(speed >= 0.5 && speed <= 2.);
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.h b/Telegram/SourceFiles/media/streaming/media_streaming_player.h
index bac478566..4625c39a0 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_player.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.h
@@ -44,10 +44,11 @@ public:
 	float64 speed() const;
 	void setSpeed(float64 speed); // 0.5 <= speed <= 2.
 
-	[[nodiscard]] bool failed() const;
 	[[nodiscard]] bool playing() const;
 	[[nodiscard]] bool buffering() const;
 	[[nodiscard]] bool paused() const;
+	[[nodiscard]] bool failed() const;
+	[[nodiscard]] bool finished() const;
 
 	[[nodiscard]] rpl::producer<Update, Error> updates() const;
 
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp
index 5285ce629..7f68d6136 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/streaming/media_streaming_utility.h"
 
 #include "media/streaming/media_streaming_common.h"
+#include "ui/image/image_prepare.h"
 
 extern "C" {
 #include <libavutil/opt.h>
@@ -20,6 +21,7 @@ namespace {
 constexpr auto kSkipInvalidDataPackets = 10;
 constexpr auto kAlignImageBy = 16;
 constexpr auto kPixelBytesSize = 4;
+constexpr auto kImageFormat = QImage::Format_ARGB32_Premultiplied;
 
 void AlignedImageBufferCleanupHandler(void* data) {
 	const auto buffer = static_cast<uchar*>(data);
@@ -39,8 +41,16 @@ void ClearFrameMemory(AVFrame *frame) {
 
 } // namespace
 
+bool GoodStorageForFrame(const QImage &storage, QSize size) {
+	return !storage.isNull()
+		&& (storage.format() == kImageFormat)
+		&& (storage.size() == size)
+		&& storage.isDetached()
+		&& IsAlignedImage(storage);
+}
+
 // Create a QImage of desired size where all the data is properly aligned.
-QImage CreateImageForOriginalFrame(QSize size) {
+QImage CreateFrameStorage(QSize size) {
 	const auto width = size.width();
 	const auto height = size.height();
 	const auto widthAlign = kAlignImageBy / kPixelBytesSize;
@@ -59,7 +69,7 @@ QImage CreateImageForOriginalFrame(QSize size) {
 		width,
 		height,
 		perLine,
-		QImage::Format_ARGB32_Premultiplied,
+		kImageFormat,
 		AlignedImageBufferCleanupHandler,
 		cleanupData);
 }
@@ -267,18 +277,26 @@ AvErrorWrap ReadNextFrame(Stream &stream) {
 	return error;
 }
 
-QImage ConvertFrame(
-		Stream &stream,
-		QSize resize,
-		QImage storage) {
+bool GoodForRequest(const QImage &image, const FrameRequest &request) {
+	if (request.resize.isEmpty()) {
+		return true;
+	} else if ((request.radius != ImageRoundRadius::None)
+		&& ((request.corners & RectPart::AllCorners) != 0)) {
+		return false;
+	}
+	return (request.resize == request.outer)
+		&& (request.resize == image.size());
+}
+
+QImage ConvertFrame(Stream &stream, QSize resize, QImage storage) {
 	Expects(stream.frame != nullptr);
 
 	const auto frame = stream.frame.get();
 	const auto frameSize = QSize(frame->width, frame->height);
 	if (frameSize.isEmpty()) {
 		LOG(("Streaming Error: Bad frame size %1,%2"
-			).arg(resize.width()
-			).arg(resize.height()));
+			).arg(frameSize.width()
+			).arg(frameSize.height()));
 		return QImage();
 	} else if (!frame->data[0]) {
 		LOG(("Streaming Error: Bad frame data."));
@@ -289,11 +307,9 @@ QImage ConvertFrame(
 	} else if (RotationSwapWidthHeight(stream.rotation)) {
 		resize.transpose();
 	}
-	if (storage.isNull()
-		|| storage.size() != resize
-		|| !storage.isDetached()
-		|| !IsAlignedImage(storage)) {
-		storage = CreateImageForOriginalFrame(resize);
+
+	if (!GoodStorageForFrame(storage, resize)) {
+		storage = CreateFrameStorage(resize);
 	}
 	const auto format = AV_PIX_FMT_BGRA;
 	const auto hasDesiredFormat = (frame->format == format)
@@ -343,7 +359,24 @@ QImage ConvertFrame(
 			return QImage();
 		}
 	}
-	ClearFrameMemory(stream.frame.get());
+	return storage;
+}
+
+QImage PrepareByRequest(
+		const QImage &original,
+		const FrameRequest &request,
+		QImage storage) {
+	Expects(!request.outer.isEmpty());
+
+	if (!GoodStorageForFrame(storage, request.outer)) {
+		storage = CreateFrameStorage(request.outer);
+	}
+	{
+		Painter p(&storage);
+		PainterHighQualityEnabler hq(p);
+		p.drawImage(QRect(QPoint(), request.outer), original);
+	}
+	// #TODO streaming later full prepare support.
 	return storage;
 }
 
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_utility.h b/Telegram/SourceFiles/media/streaming/media_streaming_utility.h
index d769f5d0c..f983d5a68 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_utility.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_utility.h
@@ -173,11 +173,19 @@ void LogError(QLatin1String method, AvErrorWrap error);
 [[nodiscard]] AvErrorWrap ProcessPacket(Stream &stream, Packet &&packet);
 [[nodiscard]] AvErrorWrap ReadNextFrame(Stream &stream);
 
-[[nodiscard]] QImage CreateImageForOriginalFrame(QSize size);
+[[nodiscard]] bool GoodForRequest(
+	const QImage &image,
+	const FrameRequest &request);
+[[nodiscard]] bool GoodStorageForFrame(const QImage &storage, QSize size);
+[[nodiscard]] QImage CreateFrameStorage(QSize size);
 [[nodiscard]] QImage ConvertFrame(
 	Stream& stream,
 	QSize resize,
 	QImage storage);
+[[nodiscard]] QImage PrepareByRequest(
+	const QImage &original,
+	const FrameRequest &request,
+	QImage storage);
 
 } // namespace Streaming
 } // namespace Media
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp
index 5712b5c7a..fb14ab4b8 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp
@@ -43,6 +43,7 @@ public:
 	void setSpeed(float64 speed);
 	void interrupt();
 	void frameDisplayed();
+	void updateFrameRequest(const FrameRequest &request);
 
 private:
 	[[nodiscard]] bool interrupted() const;
@@ -79,6 +80,7 @@ private:
 	crl::time _nextFrameDisplayTime = kTimeUnknown;
 	rpl::event_stream<crl::time> _nextFrameTimeUpdates;
 	rpl::event_stream<> _waitingForData;
+	FrameRequest _request;
 
 	bool _queued = false;
 	base::ConcurrentTimer _readFramesTimer;
@@ -150,10 +152,15 @@ void VideoTrackObject::readFrames() {
 	if (interrupted()) {
 		return;
 	}
-	const auto state = _shared->prepareState(trackTime().trackTime);
+	const auto time = trackTime().trackTime;
+	const auto dropStaleFrames = _options.dropStaleFrames;
+	const auto state = _shared->prepareState(time, dropStaleFrames);
 	state.match([&](Shared::PrepareFrame frame) {
-		if (readFrame(frame)) {
-			presentFrameIfNeeded();
+		while (readFrame(frame)) {
+			if (!dropStaleFrames || !VideoTrack::IsStale(frame, time)) {
+				presentFrameIfNeeded();
+				break;
+			}
 		}
 	}, [&](Shared::PrepareNextCheck delay) {
 		Expects(delay > 0);
@@ -183,17 +190,8 @@ bool VideoTrackObject::readFrame(not_null<Frame*> frame) {
 		_error();
 		return false;
 	}
-	frame->original = ConvertFrame(
-		_stream,
-		QSize(),
-		std::move(frame->original));
 	frame->position = position;
 	frame->displayed = kTimeUnknown;
-
-	// #TODO streaming later prepare frame
-	//frame->request
-	//frame->prepared
-
 	return true;
 }
 
@@ -202,7 +200,24 @@ void VideoTrackObject::presentFrameIfNeeded() {
 		return;
 	}
 	const auto time = trackTime();
-	const auto presented = _shared->presentFrame(time.trackTime);
+	const auto prepare = [&](not_null<Frame*> frame) {
+		frame->request = _request;
+		frame->original = ConvertFrame(
+			_stream,
+			frame->request.resize,
+			std::move(frame->original));
+		if (frame->original.isNull()) {
+			frame->prepared = QImage();
+			interrupt();
+			_error();
+			return;
+		}
+
+		VideoTrack::PrepareFrameByRequest(frame);
+
+		Ensures(VideoTrack::IsPrepared(frame));
+	};
+	const auto presented = _shared->presentFrame(time.trackTime, prepare);
 	if (presented.displayPosition != kTimeUnknown) {
 		const auto trackLeft = presented.displayPosition - time.trackTime;
 
@@ -268,6 +283,10 @@ void VideoTrackObject::frameDisplayed() {
 	queueReadFrames();
 }
 
+void VideoTrackObject::updateFrameRequest(const FrameRequest &request) {
+	_request = request;
+}
+
 bool VideoTrackObject::tryReadFirstFrame(Packet &&packet) {
 	if (ProcessPacket(_stream, std::move(packet)).failed()) {
 		return false;
@@ -412,31 +431,20 @@ not_null<VideoTrack::Frame*> VideoTrack::Shared::getFrame(int index) {
 	return &_frames[index];
 }
 
-bool VideoTrack::Shared::IsPrepared(not_null<Frame*> frame) {
-	return (frame->position != kTimeUnknown)
-		&& (frame->displayed == kTimeUnknown)
-		&& !frame->original.isNull();
-}
-
-bool VideoTrack::Shared::IsStale(
-		not_null<Frame*> frame,
-		crl::time trackTime) {
-	Expects(IsPrepared(frame));
-
-	return (frame->position < trackTime);
-}
-
-auto VideoTrack::Shared::prepareState(crl::time trackTime) -> PrepareState {
+auto VideoTrack::Shared::prepareState(
+	crl::time trackTime,
+	bool dropStaleFrames)
+-> PrepareState {
 	const auto prepareNext = [&](int index) -> PrepareState {
 		const auto frame = getFrame(index);
 		const auto next = getFrame((index + 1) % kFramesCount);
-		if (!IsPrepared(frame)) {
+		if (!IsDecoded(frame)) {
 			return frame;
-		} else if (IsStale(frame, trackTime)) {
+		} else if (dropStaleFrames && IsStale(frame, trackTime)) {
 			std::swap(*frame, *next);
 			next->displayed = kDisplaySkipped;
-			return IsPrepared(frame) ? next : frame;
-		} else if (!IsPrepared(next)) {
+			return IsDecoded(frame) ? next : frame;
+		} else if (!IsDecoded(next)) {
 			return next;
 		} else {
 			return PrepareNextCheck(frame->position - trackTime + 1);
@@ -445,7 +453,7 @@ auto VideoTrack::Shared::prepareState(crl::time trackTime) -> PrepareState {
 	const auto finishPrepare = [&](int index) {
 		const auto frame = getFrame(index);
 		// If player already awaits next frame - we ignore if it's stale.
-		return IsPrepared(frame) ? std::nullopt : PrepareState(frame);
+		return IsDecoded(frame) ? std::nullopt : PrepareState(frame);
 	};
 
 	switch (counter()) {
@@ -461,11 +469,18 @@ auto VideoTrack::Shared::prepareState(crl::time trackTime) -> PrepareState {
 	Unexpected("Counter value in VideoTrack::Shared::prepareState.");
 }
 
-auto VideoTrack::Shared::presentFrame(crl::time trackTime) -> PresentFrame {
+template <typename PrepareCallback>
+auto VideoTrack::Shared::presentFrame(
+	crl::time trackTime,
+	PrepareCallback &&prepare)
+-> PresentFrame {
 	const auto present = [&](int counter, int index) -> PresentFrame {
 		const auto frame = getFrame(index);
-		Assert(IsPrepared(frame));
 		const auto position = frame->position;
+		prepare(frame);
+		if (!IsPrepared(frame)) {
+			return { kTimeUnknown, crl::time(0) };
+		}
 
 		// Release this frame to the main thread for rendering.
 		_counter.store(
@@ -476,8 +491,8 @@ auto VideoTrack::Shared::presentFrame(crl::time trackTime) -> PresentFrame {
 	const auto nextCheckDelay = [&](int index) -> PresentFrame {
 		const auto frame = getFrame(index);
 		const auto next = getFrame((index + 1) % kFramesCount);
-		if (!IsPrepared(frame)
-			|| !IsPrepared(next)
+		if (!IsDecoded(frame)
+			|| !IsDecoded(next)
 			|| IsStale(frame, trackTime)) {
 			return { kTimeUnknown, crl::time(0) };
 		}
@@ -594,24 +609,50 @@ crl::time VideoTrack::markFrameDisplayed(crl::time now) {
 	return position;
 }
 
-QImage VideoTrack::frame(const FrameRequest &request) const {
+QImage VideoTrack::frame(const FrameRequest &request) {
 	const auto frame = _shared->frameForPaint();
-	Assert(frame != nullptr);
-	Assert(!frame->original.isNull());
+	const auto changed = (frame->request != request);
+	if (changed) {
+		frame->request = request;
+		_wrapped.with([=](Implementation &unwrapped) {
+			unwrapped.updateFrameRequest(request);
+		});
+	}
+	return PrepareFrameByRequest(frame, !changed);
+}
 
-	if (request.resize.isEmpty()) {
+QImage VideoTrack::PrepareFrameByRequest(
+		not_null<Frame*> frame,
+		bool useExistingPrepared) {
+	Expects(!frame->original.isNull());
+
+	if (GoodForRequest(frame->original, frame->request)) {
 		return frame->original;
-	} else if (frame->prepared.isNull() || frame->request != request) {
-		// #TODO streaming later prepare frame
-		//frame->request = request;
-		//frame->prepared = PrepareFrame(
-		//	frame->original,
-		//	request,
-		//	std::move(frame->prepared));
+	} else if (frame->prepared.isNull() || !useExistingPrepared) {
+		frame->prepared = PrepareByRequest(
+			frame->original,
+			frame->request,
+			std::move(frame->prepared));
 	}
 	return frame->prepared;
 }
 
+bool VideoTrack::IsDecoded(not_null<Frame*> frame) {
+	return (frame->position != kTimeUnknown)
+		&& (frame->displayed == kTimeUnknown);
+}
+
+bool VideoTrack::IsPrepared(not_null<Frame*> frame) {
+	return IsDecoded(frame)
+		&& !frame->original.isNull();
+}
+
+bool VideoTrack::IsStale(not_null<Frame*> frame, crl::time trackTime) {
+	Expects(IsDecoded(frame));
+
+	return (frame->position < trackTime);
+}
+
 rpl::producer<crl::time> VideoTrack::renderNextFrame() const {
 	return _wrapped.producer_on_main([](const Implementation &unwrapped) {
 		return unwrapped.displayFrameAt();
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h
index 2970888e8..57ef6c47c 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h
@@ -46,7 +46,7 @@ public:
 	// Called from the main thread.
 	// Returns the position of the displayed frame.
 	[[nodiscard]] crl::time markFrameDisplayed(crl::time now);
-	[[nodiscard]] QImage frame(const FrameRequest &request) const;
+	[[nodiscard]] QImage frame(const FrameRequest &request);
 	[[nodiscard]] rpl::producer<crl::time> renderNextFrame() const;
 	[[nodiscard]] rpl::producer<> waitingForData() const;
 
@@ -81,8 +81,15 @@ private:
 		void init(QImage &&cover, crl::time position);
 		[[nodiscard]] bool initialized() const;
 
-		[[nodiscard]] PrepareState prepareState(crl::time trackTime);
-		[[nodiscard]] PresentFrame presentFrame(crl::time trackTime);
+		[[nodiscard]] PrepareState prepareState(
+			crl::time trackTime,
+			bool dropStaleFrames);
+
+		// PrepareCallback(not_null<Frame*>).
+		template <typename PrepareCallback>
+		[[nodiscard]] PresentFrame presentFrame(
+			crl::time trackTime,
+			PrepareCallback &&prepare);
 
 		// Called from the main thread.
 		// Returns the position of the displayed frame.
@@ -91,10 +98,6 @@ private:
 
 	private:
 		[[nodiscard]] not_null<Frame*> getFrame(int index);
-		[[nodiscard]] static bool IsPrepared(not_null<Frame*> frame);
-		[[nodiscard]] static bool IsStale(
-			not_null<Frame*> frame,
-			crl::time trackTime);
 		[[nodiscard]] int counter() const;
 
 		static constexpr auto kCounterUninitialized = -1;
@@ -105,6 +108,15 @@ private:
 
 	};
 
+	static QImage PrepareFrameByRequest(
+		not_null<Frame*> frame,
+		bool useExistingPrepared = false);
+	[[nodiscard]] static bool IsDecoded(not_null<Frame*> frame);
+	[[nodiscard]] static bool IsPrepared(not_null<Frame*> frame);
+	[[nodiscard]] static bool IsStale(
+		not_null<Frame*> frame,
+		crl::time trackTime);
+
 	const int _streamIndex = 0;
 	const AVRational _streamTimeBase;
 	//const int _streamRotation = 0;
diff --git a/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp b/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp
index fcee65543..7a76ddb78 100644
--- a/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp
@@ -650,7 +650,7 @@ bool GroupThumbs::hidden() const {
 void GroupThumbs::checkForAnimationStart() {
 	if (_waitingForAnimationStart) {
 		_waitingForAnimationStart = false;
-		_animation.start([this] { update(); }, 0., 1., kThumbDuration);
+		_animation.start([=] { update(); }, 0., 1., kThumbDuration);
 	}
 }
 
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index ce540d31c..708ea2e77 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -20,8 +20,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/text_options.h"
 #include "media/audio/media_audio.h"
 #include "media/clip/media_clip_reader.h"
-#include "media/view/media_clip_controller.h"
+#include "media/view/media_view_playback_controls.h"
 #include "media/view/media_view_group_thumbs.h"
+#include "media/streaming/media_streaming_player.h"
+#include "media/streaming/media_streaming_loader.h"
 #include "history/history.h"
 #include "history/history_message.h"
 #include "data/data_media_types.h"
@@ -43,6 +45,7 @@ namespace Media {
 namespace View {
 namespace {
 
+constexpr auto kMsFrequency = 1000; // 1000 ms per second.
 constexpr auto kPreloadCount = 4;
 
 // Preload X message ids before and after current.
@@ -58,6 +61,62 @@ Images::Options VideoThumbOptions(not_null<DocumentData*> document) {
 		: result;
 }
 
+void PaintImageProfile(QPainter &p, const QImage &image, QRect rect, QRect fill) {
+	const auto argb = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
+	const auto rgb = image.convertToFormat(QImage::Format_RGB32);
+	const auto argbp = QPixmap::fromImage(argb);
+	const auto rgbp = QPixmap::fromImage(rgb);
+	const auto width = image.width();
+	const auto height = image.height();
+	const auto xcopies = (fill.width() + width - 1) / width;
+	const auto ycopies = (fill.height() + height - 1) / height;
+	const auto copies = xcopies * ycopies;
+	auto times = QStringList();
+	const auto bench = [&](QString label, auto &&paint) {
+		const auto single = [&](QString label) {
+			auto now = crl::now();
+			const auto push = [&] {
+				times.push_back(QString("%1").arg(crl::now() - now, 4, 10, QChar(' ')));
+				now = crl::now();
+			};
+			paint(rect);
+			push();
+			{
+				PainterHighQualityEnabler hq(p);
+				paint(rect);
+			}
+			push();
+			for (auto i = 0; i < xcopies; ++i) {
+				for (auto j = 0; j < ycopies; ++j) {
+					paint(QRect(
+						fill.topLeft() + QPoint(i * width, j * height),
+						QSize(width, height)));
+				}
+			}
+			push();
+			LOG(("FRAME (%1): %2 (copies: %3)").arg(label).arg(times.join(' ')).arg(copies));
+			times = QStringList();
+			now = crl::now();
+		};
+		p.setCompositionMode(QPainter::CompositionMode_Source);
+		single(label + " S");
+		p.setCompositionMode(QPainter::CompositionMode_SourceOver);
+		single(label + " O");
+	};
+	bench("ARGB I", [&](QRect rect) {
+		p.drawImage(rect, argb);
+	});
+	bench("RGB  I", [&](QRect rect) {
+		p.drawImage(rect, rgb);
+	});
+	bench("ARGB P", [&](QRect rect) {
+		p.drawPixmap(rect, argbp);
+	});
+	bench("RGB  P", [&](QRect rect) {
+		p.drawPixmap(rect, rgbp);
+	});
+}
+
 } // namespace
 
 struct OverlayWidget::SharedMedia {
@@ -83,10 +142,30 @@ struct OverlayWidget::Collage {
 	CollageKey key;
 };
 
+struct OverlayWidget::Streamed {
+	Streamed(
+		not_null<Data::Session*> owner,
+		std::unique_ptr<Streaming::Loader> loader,
+		QWidget *controlsParent,
+		not_null<PlaybackControls::Delegate*> controlsDelegate);
+
+	Streaming::Player player;
+	Streaming::Information info;
+	PlaybackControls controls;
+};
+
+OverlayWidget::Streamed::Streamed(
+	not_null<Data::Session*> owner,
+	std::unique_ptr<Streaming::Loader> loader,
+	QWidget *controlsParent,
+	not_null<PlaybackControls::Delegate*> controlsDelegate)
+: player(owner, std::move(loader))
+, controls(controlsParent, controlsDelegate) {
+}
+
 OverlayWidget::OverlayWidget()
 : OverlayParent(nullptr)
 , _transparentBrush(style::transparentPlaceholderBrush())
-, _animStarted(crl::now())
 , _docDownload(this, lang(lng_media_download), st::mediaviewFileLink)
 , _docSaveAs(this, lang(lng_mediaview_save_as), st::mediaviewFileLink)
 , _docCancel(this, lang(lng_cancel), st::mediaviewFileLink)
@@ -117,8 +196,11 @@ OverlayWidget::OverlayWidget()
 				}
 			});
 			subscribe(Auth().calls().currentCallChanged(), [this](Calls::Call *call) {
-				if (call && _clipController && !_videoPaused) {
-					onVideoPauseResume();
+				if (call
+					&& _streamed
+					&& !_streamed->player.paused()
+					&& !_streamed->player.finished()) {
+					playbackPauseResume();
 				}
 			});
 			subscribe(Auth().documentUpdated, [this](DocumentData *document) {
@@ -213,52 +295,72 @@ void OverlayWidget::moveToScreen() {
 	_saveMsg.moveTo((width() - _saveMsg.width()) / 2, (height() - _saveMsg.height()) / 2);
 }
 
-bool OverlayWidget::fileShown() const {
-	return !_current.isNull() || gifShown();
+bool OverlayWidget::videoShown() const {
+	return _streamed && !_streamed->info.video.cover.isNull();
 }
 
-bool OverlayWidget::fileBubbleShown() const {
-	return (!_photo && !_doc) || (_doc && !fileShown() && !_themePreviewShown);
+QSize OverlayWidget::videoSize() const {
+	Expects(videoShown());
+
+	return _streamed->info.video.size;
 }
 
-bool OverlayWidget::gifShown() const {
-	if (_gif && _gif->ready()) {
-		if (!_gif->started()) {
-			const auto streamVideo = _doc
-				&& (_doc->isVideoFile() || _doc->isVideoMessage());
-			const auto pauseOnStart = (_autoplayVideoDocument != _doc);
-			if (streamVideo && pauseOnStart && !_gif->videoPaused()) {
-				const_cast<OverlayWidget*>(this)->toggleVideoPaused();
-			}
-			const auto rounding = (_doc && _doc->isVideoMessage())
-				? ImageRoundRadius::Ellipse
-				: ImageRoundRadius::None;
-			_gif->start(
-				_gif->width() / cIntRetinaFactor(),
-				_gif->height() / cIntRetinaFactor(),
-				_gif->width() / cIntRetinaFactor(),
-				_gif->height() / cIntRetinaFactor(),
-				rounding,
-				RectPart::AllCorners);
-			const_cast<OverlayWidget*>(this)->_current = QPixmap();
-			updateMixerVideoVolume();
-			Global::RefVideoVolumeChanged().notify();
-		}
-		return true;// _gif->state() != Media::Clip::State::Error;
+bool OverlayWidget::videoIsGifv() const {
+	return _streamed && _doc->isAnimation() && !_doc->isVideoMessage();
+}
+
+QImage OverlayWidget::videoFrame() const {
+	Expects(videoShown());
+
+	auto request = Media::Streaming::FrameRequest();
+	//request.radius = (_doc && _doc->isVideoMessage())
+	//	? ImageRoundRadius::Ellipse
+	//	: ImageRoundRadius::None;
+	return _streamed->player.ready()
+		? _streamed->player.frame(request)
+		: _streamed->info.video.cover;
+}
+
+crl::time OverlayWidget::streamedPosition() const {
+	Expects(_streamed != nullptr);
+
+	const auto result = std::max(
+		_streamed->info.audio.state.position,
+		_streamed->info.video.state.position);
+	return (result != kTimeUnknown) ? result : crl::time(0);
+}
+
+crl::time OverlayWidget::streamedDuration() const {
+	Expects(_streamed != nullptr);
+
+	const auto result = std::max(
+		_streamed->info.audio.state.duration,
+		_streamed->info.video.state.duration);
+	if (result != kTimeUnknown) {
+		return result;
 	}
-	return false;
+	const auto duration = _doc->song()
+		? _doc->song()->duration
+		: _doc->duration();
+	return (duration > 0) ? duration * crl::time(1000) : kTimeUnknown;
 }
 
-void OverlayWidget::stopGif() {
-	_gif = nullptr;
-	_videoPaused = _videoStopped = _videoIsSilent = false;
+bool OverlayWidget::documentContentShown() const {
+	return _doc && (!_current.isNull() || videoShown());
+}
+
+bool OverlayWidget::documentBubbleShown() const {
+	return (!_photo && !_doc)
+		|| (_doc && !_themePreviewShown && _current.isNull() && !_streamed);
+}
+
+void OverlayWidget::clearStreaming() {
 	_fullScreenVideo = false;
-	_clipController.destroy();
-	disconnect(Media::Player::mixer(), SIGNAL(updated(const AudioMsgId&)), this, SLOT(onVideoPlayProgress(const AudioMsgId&)));
+	_streamed = nullptr;
 }
 
 void OverlayWidget::documentUpdated(DocumentData *doc) {
-	if (fileBubbleShown() && _doc && _doc == doc) {
+	if (documentBubbleShown() && _doc && _doc == doc) {
 		if ((_doc->loading() && _docCancel->isHidden()) || (!_doc->loading() && !_docCancel->isHidden())) {
 			updateControls();
 		} else if (_doc->loading()) {
@@ -276,7 +378,7 @@ void OverlayWidget::changingMsgId(not_null<HistoryItem*> row, MsgId newId) {
 }
 
 void OverlayWidget::updateDocSize() {
-	if (!_doc || !fileBubbleShown()) return;
+	if (!_doc || !documentBubbleShown()) return;
 
 	if (_doc->loading()) {
 		quint64 ready = _doc->loadOffset(), total = _doc->size;
@@ -325,7 +427,7 @@ void OverlayWidget::refreshNavVisibility() {
 }
 
 void OverlayWidget::updateControls() {
-	if (_doc && fileBubbleShown()) {
+	if (_doc && documentBubbleShown()) {
 		if (_doc->loading()) {
 			_docDownload->hide();
 			_docSaveAs->hide();
@@ -355,7 +457,9 @@ void OverlayWidget::updateControls() {
 
 	updateThemePreviewGeometry();
 
-	_saveVisible = ((_photo && _photo->loaded()) || (_doc && (_doc->loaded(DocumentData::FilePathResolveChecked) || (!fileShown() && (_photo || _doc)))));
+	_saveVisible = (_photo && _photo->loaded())
+		|| (_doc && (_doc->loaded(DocumentData::FilePathResolveChecked)
+			|| !documentContentShown()));
 	_saveNav = myrtlrect(width() - st::mediaviewIconSize.width() * 2, height() - st::mediaviewIconSize.height(), st::mediaviewIconSize.width(), st::mediaviewIconSize.height());
 	_saveNavIcon = centerrect(_saveNav, st::mediaviewSave);
 	_moreNav = myrtlrect(width() - st::mediaviewIconSize.width(), height() - st::mediaviewIconSize.height(), st::mediaviewIconSize.width(), st::mediaviewIconSize.height());
@@ -423,8 +527,8 @@ void OverlayWidget::refreshCaptionGeometry() {
 		_groupThumbs = nullptr;
 		_groupThumbsRect = QRect();
 	}
-	const auto captionBottom = _clipController
-		? (_clipController->y() - st::mediaviewCaptionMargin.height())
+	const auto captionBottom = (_streamed && !videoIsGifv())
+		? (_streamed->controls.y() - st::mediaviewCaptionMargin.height())
 		: _groupThumbs
 		? _groupThumbsTop
 		: height() - st::mediaviewCaptionMargin.height();
@@ -460,7 +564,7 @@ void OverlayWidget::updateActions() {
 	if (_doc && !_doc->filepath(DocumentData::FilePathResolveChecked).isEmpty()) {
 		_actions.push_back({ lang((cPlatform() == dbipMac || cPlatform() == dbipMacOld) ? lng_context_show_in_finder : lng_context_show_in_folder), SLOT(onShowInFolder()) });
 	}
-	if ((_doc && fileShown()) || (_photo && _photo->loaded())) {
+	if ((_doc && documentContentShown()) || (_photo && _photo->loaded())) {
 		_actions.push_back({ lang(lng_mediaview_copy), SLOT(onCopy()) });
 	}
 	if (_photo && _photo->hasSticker) {
@@ -571,6 +675,10 @@ void OverlayWidget::updateCursor() {
 		: (_over == OverNone ? style::cur_default : style::cur_pointer));
 }
 
+QRect OverlayWidget::contentRect() const {
+	return { _x, _y, _w, _h };
+}
+
 float64 OverlayWidget::radialProgress() const {
 	if (_doc) {
 		return _doc->progress();
@@ -691,7 +799,7 @@ void OverlayWidget::zoomReset() {
 		newZoom = 0;
 	}
 	_x = -_width / 2;
-	_y = -((gifShown() ? _gif->height() : (_current.height() / cIntRetinaFactor())) / 2);
+	_y = -_height / 2;
 	float64 z = (_zoom == ZoomToScreenLevel) ? _zoomToScreen : _zoom;
 	if (z >= 0) {
 		_x = qRound(_x * (z + 1));
@@ -724,7 +832,7 @@ void OverlayWidget::clearData() {
 		_a_state.stop();
 	}
 	if (!_animOpacities.isEmpty()) _animOpacities.clear();
-	stopGif();
+	clearStreaming();
 	delete _menu;
 	_menu = nullptr;
 	setContext(std::nullopt);
@@ -754,7 +862,7 @@ void OverlayWidget::showSaveMsgFile() {
 }
 
 void OverlayWidget::updateMixerVideoVolume() const {
-	if (_doc && (_doc->isVideoFile() || _doc->isVideoMessage())) {
+	if (_streamed) {
 		Media::Player::mixer()->setVideoVolume(Global::VideoVolume());
 	}
 }
@@ -768,8 +876,8 @@ void OverlayWidget::activateControls() {
 		_controlsHideTimer.start(int(st::mediaviewWaitHide));
 	}
 	if (_fullScreenVideo) {
-		if (_clipController) {
-			_clipController->showAnimated();
+		if (_streamed) {
+			_streamed->controls.showAnimated();
 		}
 	}
 	if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) {
@@ -785,14 +893,14 @@ void OverlayWidget::onHideControls(bool force) {
 		if (!_dropdown->isHidden()
 			|| _menu
 			|| _mousePressed
-			|| (_fullScreenVideo && _clipController && _clipController->geometry().contains(_lastMouseMovePos))) {
+			|| (_fullScreenVideo
+				&& !videoIsGifv()
+				&& _streamed->controls.geometry().contains(_lastMouseMovePos))) {
 			return;
 		}
 	}
 	if (_fullScreenVideo) {
-		if (_clipController) {
-			_clipController->hideAnimated();
-		}
+		_streamed->controls.hideAnimated();
 	}
 	if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) return;
 
@@ -881,8 +989,8 @@ void OverlayWidget::onSaveAs() {
 
 			if (_doc->data().isEmpty()) location.accessDisable();
 		} else {
-			if (!fileShown()) {
-				DocumentSaveClickHandler::Save(fileOrigin(), _doc, true);
+			if (!documentContentShown()) {
+				DocumentSaveClickHandler::Save(fileOrigin(), _doc, App::histItemById(_msgid), true);
 				updateControls();
 			} else {
 				_saveVisible = false;
@@ -934,51 +1042,6 @@ void OverlayWidget::onDocClick() {
 	}
 }
 
-void OverlayWidget::clipCallback(Media::Clip::Notification notification) {
-	using namespace Media::Clip;
-
-	if (!_gif) return;
-
-	switch (notification) {
-	case NotificationReinit: {
-		if (auto item = App::histItemById(_msgid)) {
-			if (_gif->state() == State::Error) {
-				stopGif();
-				updateControls();
-				update();
-				break;
-			} else if (_gif->state() == State::Finished) {
-				_videoPositionMs = _videoDurationMs;
-				_videoStopped = true;
-				updateSilentVideoPlaybackState();
-			} else {
-				_videoIsSilent = _doc && (_doc->isVideoFile() || _doc->isVideoMessage()) && !_gif->hasAudio();
-				_videoDurationMs = _gif->getDurationMs();
-				_videoPositionMs = _gif->getPositionMs();
-				if (_videoIsSilent) {
-					updateSilentVideoPlaybackState();
-				}
-			}
-			displayDocument(_doc, item);
-		} else {
-			stopGif();
-			updateControls();
-			update();
-		}
-	} break;
-
-	case NotificationRepaint: {
-		if (!_gif->currentDisplayed()) {
-			_videoPositionMs = _gif->getPositionMs();
-			if (_videoIsSilent) {
-				updateSilentVideoPlaybackState();
-			}
-			update(_x, _y, _w, _h);
-		}
-	} break;
-	}
-}
-
 PeerData *OverlayWidget::ui_getPeerForMouseAction() {
 	return _history ? _history->peer.get() : nullptr;
 }
@@ -1013,8 +1076,11 @@ void OverlayWidget::onDownload() {
 			}
 			location.accessDisable();
 		} else {
-			if (!fileShown()) {
-				DocumentSaveClickHandler::Save(fileOrigin(), _doc);
+			if (!documentContentShown()) {
+				DocumentSaveClickHandler::Save(
+					fileOrigin(),
+					_doc,
+					App::histItemById(_msgid));
 				updateControls();
 			} else {
 				_saveVisible = false;
@@ -1102,8 +1168,8 @@ void OverlayWidget::onCopy() {
 	if (_doc) {
 		if (!_current.isNull()) {
 			QApplication::clipboard()->setPixmap(_current);
-		} else if (gifShown()) {
-			QApplication::clipboard()->setPixmap(_gif->frameOriginal());
+		} else if (videoShown()) {
+			QApplication::clipboard()->setImage(videoFrame());
 		}
 	} else {
 		if (!_photo || !_photo->loaded()) return;
@@ -1557,7 +1623,7 @@ void OverlayWidget::displayPhoto(not_null<PhotoData*> photo, HistoryItem *item)
 		displayDocument(nullptr, item);
 		return;
 	}
-	stopGif();
+	clearStreaming();
 	destroyThemePreview();
 	_doc = _autoplayVideoDocument = nullptr;
 	_fullScreenVideo = false;
@@ -1593,6 +1659,7 @@ void OverlayWidget::displayPhoto(not_null<PhotoData*> photo, HistoryItem *item)
 	_x = (width() - _w) / 2;
 	_y = (height() - _h) / 2;
 	_width = _w;
+	_height = _h;
 	if (_msgid && item) {
 		_from = item->senderOriginal();
 	} else {
@@ -1610,13 +1677,16 @@ void OverlayWidget::destroyThemePreview() {
 	_themeCancel.destroy();
 }
 
-void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) { // empty messages shown as docs: doc can be NULL
-	auto documentChanged = (!doc || doc != _doc || (item && item->fullId() != _msgid));
+// Empty messages shown as docs: doc can be nullptr.
+void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) {
+	const auto documentChanged = !doc
+		|| (doc != _doc)
+		|| (item && item->fullId() != _msgid);
 	if (documentChanged || (!doc->isAnimation() && !doc->isVideoFile())) {
 		_fullScreenVideo = false;
 		_current = QPixmap();
-		stopGif();
-	} else if (gifShown()) {
+		clearStreaming();
+	} else if (videoShown()) {
 		_current = QPixmap();
 	}
 	if (documentChanged || !doc->isTheme()) {
@@ -1648,8 +1718,8 @@ void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) { // e
 		} else {
 			_doc->automaticLoad(fileOrigin(), item);
 
-			if (_doc->isAnimation() || _doc->isVideoFile()) {
-				initAnimation();
+			if (_doc->canBePlayed()) {
+				initStreaming();
 			} else if (_doc->isTheme()) {
 				initThemePreview();
 			} else {
@@ -1665,7 +1735,7 @@ void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) { // e
 	}
 
 	_docIconRect = QRect((width() - st::mediaviewFileIconSize) / 2, (height() - st::mediaviewFileIconSize) / 2, st::mediaviewFileIconSize, st::mediaviewFileIconSize);
-	if (fileBubbleShown()) {
+	if (documentBubbleShown()) {
 		if (!_doc || !_doc->hasThumbnail()) {
 			int32 colorIndex = documentColorIndex(_doc, _docExt);
 			_docIconColor = documentColor(colorIndex);
@@ -1723,14 +1793,16 @@ void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) { // e
 		_current.setDevicePixelRatio(cRetinaFactor());
 		_w = ConvertScale(_current.width());
 		_h = ConvertScale(_current.height());
-	} else {
-		_w = ConvertScale(_gif->width());
-		_h = ConvertScale(_gif->height());
+	} else if (videoShown()) {
+		const auto contentSize = ConvertScale(videoSize());
+		_w = contentSize.width();
+		_h = contentSize.height();
 	}
 	if (isHidden()) {
 		moveToScreen();
 	}
 	_width = _w;
+	_height = _h;
 	if (_w > 0 && _h > 0) {
 		_zoomToScreen = float64(width()) / _w;
 		if (_h * _zoomToScreen > height()) {
@@ -1804,61 +1876,88 @@ void OverlayWidget::displayFinished() {
 	}
 }
 
-void OverlayWidget::initAnimation() {
+void OverlayWidget::initStreaming() {
 	Expects(_doc != nullptr);
-	Expects(_doc->isAnimation() || _doc->isVideoFile());
+	Expects(_doc->canBePlayed());
 
-	auto &location = _doc->location(true);
-	if (!_doc->data().isEmpty()) {
-		createClipReader();
-	} else if (location.accessEnable()) {
-		createClipReader();
-		location.accessDisable();
-	} else if (_doc->dimensions.width() && _doc->dimensions.height()) {
+	if (_streamed) {
+		return;
+	}
+	initStreamingThumbnail();
+	createStreamingObjects();
+
+	Core::App().updateNonIdle();
+	_streamed->player.updates(
+	) | rpl::start_with_next_error([=](Streaming::Update &&update) {
+		handleStreamingUpdate(std::move(update));
+	}, [=](Streaming::Error &&error) {
+		handleStreamingError(std::move(error));
+	}, _streamed->controls.lifetime());
+
+	restartAtSeekPosition(0);
+}
+
+void OverlayWidget::initStreamingThumbnail() {
+	if (!_doc->hasThumbnail()) {
+		return;
+	}
+	if (_doc->dimensions.width() && _doc->dimensions.height()) {
 		auto w = _doc->dimensions.width();
 		auto h = _doc->dimensions.height();
-		_current = (_doc->hasThumbnail()
-			? _doc->thumbnail()
-			: Image::Empty().get())->pixNoCache(fileOrigin(), w, h, VideoThumbOptions(_doc), w / cIntRetinaFactor(), h / cIntRetinaFactor());
+		_current = _doc->thumbnail()->pixNoCache(fileOrigin(), w, h, VideoThumbOptions(_doc), w / cIntRetinaFactor(), h / cIntRetinaFactor());
 		_current.setDevicePixelRatio(cRetinaFactor());
-	} else if (_doc->hasThumbnail()) {
-		_current = _doc->thumbnail()->pixNoCache(fileOrigin(), _doc->thumbnail()->width(), _doc->thumbnail()->height(), VideoThumbOptions(_doc), st::mediaviewFileIconSize, st::mediaviewFileIconSize);
 	} else {
-		_current = Image::Empty()->pixNoCache({}, Image::Empty()->width(), Image::Empty()->height(), VideoThumbOptions(_doc), st::mediaviewFileIconSize, st::mediaviewFileIconSize);
+		_current = _doc->thumbnail()->pixNoCache(fileOrigin(), _doc->thumbnail()->width(), _doc->thumbnail()->height(), VideoThumbOptions(_doc), st::mediaviewFileIconSize, st::mediaviewFileIconSize);
 	}
 }
 
-void OverlayWidget::createClipReader() {
-	if (_gif) return;
+void OverlayWidget::createStreamingObjects() {
+	_streamed = std::make_unique<Streamed>(
+		&_doc->owner(),
+		_doc->createStreamingLoader(fileOrigin()),
+		this,
+		static_cast<PlaybackControls::Delegate *>(this));
 
-	Expects(_doc != nullptr);
-	Expects(_doc->isAnimation() || _doc->isVideoFile());
-
-	if (_doc->dimensions.width() && _doc->dimensions.height()) {
-		int w = _doc->dimensions.width();
-		int h = _doc->dimensions.height();
-		_current = (_doc->hasThumbnail()
-			? _doc->thumbnail()
-			: Image::Empty().get())->pixNoCache(fileOrigin(), w, h, VideoThumbOptions(_doc), w / cIntRetinaFactor(), h / cIntRetinaFactor());
-		_current.setDevicePixelRatio(cRetinaFactor());
-	} else if (_doc->hasThumbnail()) {
-		_current = _doc->thumbnail()->pixNoCache(fileOrigin(), _doc->thumbnail()->width(), _doc->thumbnail()->height(), VideoThumbOptions(_doc), st::mediaviewFileIconSize, st::mediaviewFileIconSize);
+	if (videoIsGifv()) {
+		_streamed->controls.hide();
 	} else {
-		_current = Image::Empty()->pixNoCache({}, Image::Empty()->width(), Image::Empty()->height(), VideoThumbOptions(_doc), st::mediaviewFileIconSize, st::mediaviewFileIconSize);
+		refreshClipControllerGeometry();
+		_streamed->controls.show();
 	}
-	auto mode = (_doc->isVideoFile() || _doc->isVideoMessage())
-		? Media::Clip::Reader::Mode::Video
-		: Media::Clip::Reader::Mode::Gif;
-	_gif = Media::Clip::MakeReader(_doc, _msgid, [this](Media::Clip::Notification notification) {
-		clipCallback(notification);
-	}, mode);
+}
 
-	// Correct values will be set when gif gets inited.
-	_videoPaused = _videoIsSilent = _videoStopped = false;
-	_videoPositionMs = 0ULL;
-	_videoDurationMs = _doc->duration() * 1000ULL;
+void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) {
+	using namespace Streaming;
 
-	createClipController();
+	update.data.match([&](Information &update) {
+		_streamed->info = std::move(update);
+		this->update(contentRect());
+	}, [&](PreloadedVideo &update) {
+		_streamed->info.video.state.receivedTill = update.till;
+		//updatePlaybackState();
+	}, [&](UpdateVideo &update) {
+		_streamed->info.video.state.position = update.position;
+		this->update(contentRect());
+		updatePlaybackState();
+	}, [&](PreloadedAudio &update) {
+		_streamed->info.audio.state.receivedTill = update.till;
+		//updatePlaybackState();
+	}, [&](UpdateAudio & update) {
+		_streamed->info.audio.state.position = update.position;
+		updatePlaybackState();
+	}, [&](WaitingForData) {
+	}, [&](MutedByOther) {
+	}, [&](Finished) {
+		const auto finishTrack = [](Media::Streaming::TrackState &state) {
+			state.position = state.receivedTill = state.duration;
+		};
+		finishTrack(_streamed->info.audio.state);
+		finishTrack(_streamed->info.video.state);
+		updatePlaybackState();
+	});
+}
+
+void OverlayWidget::handleStreamingError(Streaming::Error &&error) {
 }
 
 void OverlayWidget::initThemePreview() {
@@ -1912,27 +2011,8 @@ void OverlayWidget::initThemePreview() {
 	}
 }
 
-void OverlayWidget::createClipController() {
-	Expects(_doc != nullptr);
-	if (!_doc->isVideoFile() && !_doc->isVideoMessage()) return;
-
-	_clipController.create(this);
-	refreshClipControllerGeometry();
-	_clipController->show();
-
-	connect(_clipController, SIGNAL(playPressed()), this, SLOT(onVideoPauseResume()));
-	connect(_clipController, SIGNAL(pausePressed()), this, SLOT(onVideoPauseResume()));
-	connect(_clipController, SIGNAL(seekProgress(crl::time)), this, SLOT(onVideoSeekProgress(crl::time)));
-	connect(_clipController, SIGNAL(seekFinished(crl::time)), this, SLOT(onVideoSeekFinished(crl::time)));
-	connect(_clipController, SIGNAL(volumeChanged(float64)), this, SLOT(onVideoVolumeChanged(float64)));
-	connect(_clipController, SIGNAL(toFullScreenPressed()), this, SLOT(onVideoToggleFullScreen()));
-	connect(_clipController, SIGNAL(fromFullScreenPressed()), this, SLOT(onVideoToggleFullScreen()));
-
-	connect(Media::Player::mixer(), SIGNAL(updated(const AudioMsgId&)), this, SLOT(onVideoPlayProgress(const AudioMsgId&)));
-}
-
 void OverlayWidget::refreshClipControllerGeometry() {
-	if (!_clipController) {
+	if (!_streamed || videoIsGifv()) {
 		return;
 	}
 
@@ -1943,144 +2023,131 @@ void OverlayWidget::refreshClipControllerGeometry() {
 	const auto controllerBottom = _groupThumbs
 		? _groupThumbsTop
 		: height();
-	_clipController->setGeometry(
-		(width() - _clipController->width()) / 2,
-		controllerBottom - _clipController->height() - st::mediaviewCaptionPadding.bottom() - st::mediaviewCaptionMargin.height(),
+	_streamed->controls.setGeometry(
+		(width() - _streamed->controls.width()) / 2,
+		controllerBottom - _streamed->controls.height() - st::mediaviewCaptionPadding.bottom() - st::mediaviewCaptionMargin.height(),
 		st::mediaviewControllerSize.width(),
 		st::mediaviewControllerSize.height());
-	Ui::SendPendingMoveResizeEvents(_clipController);
+	Ui::SendPendingMoveResizeEvents(&_streamed->controls);
 }
 
-void OverlayWidget::onVideoPauseResume() {
-	if (!_gif) return;
+void OverlayWidget::playbackControlsPlay() {
+	playbackPauseResume();
+}
 
-	if (auto item = App::histItemById(_msgid)) {
-		if (_gif->state() == Media::Clip::State::Error) {
+void OverlayWidget::playbackControlsPause() {
+	playbackPauseResume();
+}
+
+void OverlayWidget::playbackControlsToFullScreen() {
+	playbackToggleFullScreen();
+}
+
+void OverlayWidget::playbackControlsFromFullScreen() {
+	playbackToggleFullScreen();
+}
+
+void OverlayWidget::playbackPauseResume() {
+	Expects(_streamed != nullptr);
+
+	if (const auto item = App::histItemById(_msgid)) {
+		if (_streamed->player.failed()) {
 			displayDocument(_doc, item);
-		} else if (_gif->state() == Media::Clip::State::Finished) {
-			restartVideoAtSeekPosition(0);
+		} else if (_streamed->player.finished()) {
+			restartAtSeekPosition(0);
 		} else {
-			toggleVideoPaused();
+			togglePauseResume();
 		}
 	} else {
-		stopGif();
+		clearStreaming();
 		updateControls();
 		update();
 	}
 }
 
-void OverlayWidget::toggleVideoPaused() {
-	_gif->pauseResumeVideo();
-	_videoPaused = _gif->videoPaused();
-	if (_videoIsSilent) {
-		updateSilentVideoPlaybackState();
+void OverlayWidget::togglePauseResume() {
+	Expects(_streamed != nullptr);
+
+	if (_streamed->player.paused()) {
+		_streamed->player.resume();
+	} else {
+		_streamed->player.pause();
 	}
 }
 
-void OverlayWidget::restartVideoAtSeekPosition(crl::time positionMs) {
-	// Seek works bad: it seeks to the next keyframe after positionMs.
-	// At least let user to seek to the beginning of the video.
-	if (positionMs < 1000
-		&& (!_videoDurationMs || (positionMs * 20 < _videoDurationMs))) {
-		positionMs = 0;
-	}
+void OverlayWidget::restartAtSeekPosition(crl::time position) {
+	Expects(_streamed != nullptr);
 
 	_autoplayVideoDocument = _doc;
 
-	if (_current.isNull()) {
-		auto rounding = (_doc && _doc->isVideoMessage()) ? ImageRoundRadius::Ellipse : ImageRoundRadius::None;
-		_current = _gif->current(_gif->width() / cIntRetinaFactor(), _gif->height() / cIntRetinaFactor(), _gif->width() / cIntRetinaFactor(), _gif->height() / cIntRetinaFactor(), rounding, RectPart::AllCorners, crl::now());
+	if (_current.isNull() && videoShown()) {
+		_current = Images::PixmapFast(videoFrame());
 	}
-	_gif = Media::Clip::MakeReader(_doc, _msgid, [this](Media::Clip::Notification notification) {
-		clipCallback(notification);
-	}, Media::Clip::Reader::Mode::Video, positionMs);
+	auto options = Streaming::PlaybackOptions();
+	options.position = position;
+	_streamed->player.play(options);
 
-	// Correct values will be set when gif gets inited.
-	_videoPaused = _videoIsSilent = _videoStopped = false;
-	_videoPositionMs = positionMs;
-
-	Media::Player::TrackState state;
-	state.state = Media::Player::State::Playing;
-	state.position = _videoPositionMs;
-	state.length = _videoDurationMs;
-	state.frequency = _videoFrequencyMs;
-	updateVideoPlaybackState(state);
+	_streamed->info.audio.state.position
+		= _streamed->info.video.state.position
+		= position;
+	updatePlaybackState();
 }
 
-void OverlayWidget::onVideoSeekProgress(crl::time positionMs) {
-	if (!_videoPaused && !_videoStopped) {
-		onVideoPauseResume();
+void OverlayWidget::playbackControlsSeekProgress(crl::time position) {
+	Expects(_streamed != nullptr);
+
+	if (!_streamed->player.paused() && !_streamed->player.finished()) {
+		playbackControlsPause();
 	}
 }
 
-void OverlayWidget::onVideoSeekFinished(crl::time positionMs) {
-	restartVideoAtSeekPosition(positionMs);
+void OverlayWidget::playbackControlsSeekFinished(crl::time position) {
+	restartAtSeekPosition(position);
 }
 
-void OverlayWidget::onVideoVolumeChanged(float64 volume) {
+void OverlayWidget::playbackControlsVolumeChanged(float64 volume) {
 	Global::SetVideoVolume(volume);
 	updateMixerVideoVolume();
 	Global::RefVideoVolumeChanged().notify();
 }
 
-void OverlayWidget::onVideoToggleFullScreen() {
-	if (!_clipController) return;
+void OverlayWidget::playbackToggleFullScreen() {
+	Expects(_streamed != nullptr);
 
+	if (!videoShown()) {
+		return;
+	}
 	_fullScreenVideo = !_fullScreenVideo;
 	if (_fullScreenVideo) {
 		_fullScreenZoomCache = _zoom;
 		setZoomLevel(ZoomToScreenLevel);
 	} else {
 		setZoomLevel(_fullScreenZoomCache);
-		_clipController->showAnimated();
+		_streamed->controls.showAnimated();
 	}
 
-	_clipController->setInFullScreen(_fullScreenVideo);
+	_streamed->controls.setInFullScreen(_fullScreenVideo);
 	updateControls();
 	update();
 }
 
-void OverlayWidget::onVideoPlayProgress(const AudioMsgId &audioId) {
-	if (!_gif || _gif->audioMsgId() != audioId) {
-		return;
-	}
+void OverlayWidget::updatePlaybackState() {
+	Expects(_streamed != nullptr);
 
-	auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Video);
-	if (state.id == _gif->audioMsgId()) {
-		if (state.length) {
-			updateVideoPlaybackState(state);
-		}
-		Core::App().updateNonIdle();
+	auto state = Player::TrackState();
+	state.state = _streamed->player.finished()
+		? Player::State::StoppedAtEnd
+		: _streamed->player.paused()
+		? Player::State::Paused
+		: Player::State::Playing;
+	state.position = streamedPosition();
+	state.length = streamedDuration();
+	state.frequency = kMsFrequency;
+	if (state.position != kTimeUnknown && state.length != kTimeUnknown) {
+		_streamed->controls.updatePlayback(state);
 	}
 }
 
-void OverlayWidget::updateVideoPlaybackState(const Media::Player::TrackState &state) {
-	if (state.frequency) {
-		if (Media::Player::IsStopped(state.state)) {
-			_videoStopped = true;
-		}
-		_clipController->updatePlayback(state);
-	} else { // Audio has stopped already.
-		_videoIsSilent = true;
-		updateSilentVideoPlaybackState();
-	}
-}
-
-void OverlayWidget::updateSilentVideoPlaybackState() {
-	Media::Player::TrackState state;
-	if (_videoPaused) {
-		state.state = Media::Player::State::Paused;
-	} else if (_videoPositionMs == _videoDurationMs) {
-		state.state = Media::Player::State::StoppedAtEnd;
-	} else {
-		state.state = Media::Player::State::Playing;
-	}
-	state.position = _videoPositionMs;
-	state.length = _videoDurationMs;
-	state.frequency = _videoFrequencyMs;
-	updateVideoPlaybackState(state);
-}
-
 void OverlayWidget::validatePhotoImage(Image *image, bool blurred) {
 	if (!image || !image->loaded()) {
 		if (!blurred) {
@@ -2091,7 +2158,7 @@ void OverlayWidget::validatePhotoImage(Image *image, bool blurred) {
 		return;
 	}
 	const auto w = _width * cIntRetinaFactor();
-	const auto h = int((_photo->height() * (qreal(w) / qreal(_photo->width()))) + 0.9999);
+	const auto h = _height * cIntRetinaFactor();
 	_current = image->pixNoCache(
 		fileOrigin(),
 		w,
@@ -2114,11 +2181,17 @@ void OverlayWidget::validatePhotoCurrentImage() {
 }
 
 void OverlayWidget::paintEvent(QPaintEvent *e) {
-	QRect r(e->rect());
-	QRegion region(e->region());
-	QVector<QRect> rs(region.rects());
+	const auto r = e->rect();
+	const auto &region = e->region();
+	const auto rects = region.rects();
+
+	const auto contentShown = _photo || documentContentShown();
+	const auto bgRects = contentShown
+		? (region - contentRect()).rects()
+		: rects;
 
 	auto ms = crl::now();
+	const auto guard = gsl::finally([&] { LOG(("FULL FRAME: %1").arg(crl::now() - ms)); });
 
 	Painter p(this);
 
@@ -2127,41 +2200,44 @@ void OverlayWidget::paintEvent(QPaintEvent *e) {
 	p.setClipRegion(region);
 
 	// main bg
-	QPainter::CompositionMode m = p.compositionMode();
+	const auto m = p.compositionMode();
 	p.setCompositionMode(QPainter::CompositionMode_Source);
-	if (_fullScreenVideo) {
-		for (int i = 0, l = region.rectCount(); i < l; ++i) {
-			p.fillRect(rs.at(i), st::mediaviewVideoBg);
-		}
-		if (_doc && _doc->isVideoMessage()) {
-			p.setCompositionMode(m);
-		}
-	} else {
-		for (int i = 0, l = region.rectCount(); i < l; ++i) {
-			p.fillRect(rs.at(i), st::mediaviewBg);
-		}
-		p.setCompositionMode(m);
+	const auto bgColor = _fullScreenVideo ? st::mediaviewVideoBg : st::mediaviewBg;
+	for (const auto &rect : bgRects) {
+		p.fillRect(rect, bgColor);
 	}
+	p.setCompositionMode(m);
 
 	// photo
 	if (_photo) {
 		validatePhotoCurrentImage();
 	}
 	p.setOpacity(1);
-	if (_photo || fileShown()) {
-		QRect imgRect(_x, _y, _w, _h);
-		if (imgRect.intersects(r)) {
-			const auto rounding = (_doc && _doc->isVideoMessage()) ? ImageRoundRadius::Ellipse : ImageRoundRadius::None;
-			const auto toDraw = (_current.isNull() && _gif) ? _gif->current(_gif->width() / cIntRetinaFactor(), _gif->height() / cIntRetinaFactor(), _gif->width() / cIntRetinaFactor(), _gif->height() / cIntRetinaFactor(), rounding, RectPart::AllCorners, ms) : _current;
-			if (!_gif && (!_doc || !_doc->getStickerLarge()) && (toDraw.hasAlpha() || toDraw.isNull())) {
-				p.fillRect(imgRect, _transparentBrush);
-			}
-			if (!toDraw.isNull()) {
-				if (toDraw.width() != _w * cIntRetinaFactor()) {
+	if (contentShown) {
+		const auto rect = contentRect();
+		if (rect.intersects(r)) {
+			if (videoShown()) {
+				const auto image = videoFrame();
+				if (image.width() != _w) {
+					//if (_fullScreenVideo) {
+					//	const auto fill = rect.intersected(this->rect());
+					//	PaintImageProfile(p, image, rect, fill);
+					//} else {
 					PainterHighQualityEnabler hq(p);
-					p.drawPixmap(QRect(_x, _y, _w, _h), toDraw);
+					p.drawImage(rect, image);
+					//}
 				} else {
-					p.drawPixmap(_x, _y, toDraw);
+					p.drawImage(rect.topLeft(), image);
+				}
+			} else if (!_current.isNull()) {
+				if ((!_doc || !_doc->getStickerLarge()) && _current.hasAlpha()) {
+					p.fillRect(rect, _transparentBrush);
+				}
+				if (_current.width() != _w * cIntRetinaFactor()) {
+					PainterHighQualityEnabler hq(p);
+					p.drawPixmap(rect, _current);
+				} else {
+					p.drawPixmap(rect.topLeft(), _current);
 				}
 			}
 
@@ -2192,39 +2268,37 @@ void OverlayWidget::paintEvent(QPaintEvent *e) {
 			} else if (_doc) {
 				paintDocRadialLoading(p, radial, radialOpacity);
 			}
-
-			if (_saveMsgStarted) {
-				auto ms = crl::now();
-				float64 dt = float64(ms) - _saveMsgStarted, hidingDt = dt - st::mediaviewSaveMsgShowing - st::mediaviewSaveMsgShown;
-				if (dt < st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + st::mediaviewSaveMsgHiding) {
-					if (hidingDt >= 0 && _saveMsgOpacity.to() > 0.5) {
-						_saveMsgOpacity.start(0);
-					}
-					float64 progress = (hidingDt >= 0) ? (hidingDt / st::mediaviewSaveMsgHiding) : (dt / st::mediaviewSaveMsgShowing);
-					_saveMsgOpacity.update(qMin(progress, 1.), anim::linear);
-                    if (_saveMsgOpacity.current() > 0) {
-						p.setOpacity(_saveMsgOpacity.current());
-						App::roundRect(p, _saveMsg, st::mediaviewSaveMsgBg, MediaviewSaveCorners);
-						st::mediaviewSaveMsgCheck.paint(p, _saveMsg.topLeft() + st::mediaviewSaveMsgCheckPos, width());
-
-						p.setPen(st::mediaviewSaveMsgFg);
-						p.setTextPalette(st::mediaviewTextPalette);
-						_saveMsgText.draw(p, _saveMsg.x() + st::mediaviewSaveMsgPadding.left(), _saveMsg.y() + st::mediaviewSaveMsgPadding.top(), _saveMsg.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right());
-						p.restoreTextPalette();
-						p.setOpacity(1);
-					}
-					if (!_blurred) {
-                        auto nextFrame = (dt < st::mediaviewSaveMsgShowing || hidingDt >= 0) ? int(AnimationTimerDelta) : (st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + 1 - dt);
-						_saveMsgUpdater.start(nextFrame);
-					}
-				} else {
-					_saveMsgStarted = 0;
+		}
+		if (_saveMsgStarted && _saveMsg.intersects(r)) {
+			float64 dt = float64(ms) - _saveMsgStarted, hidingDt = dt - st::mediaviewSaveMsgShowing - st::mediaviewSaveMsgShown;
+			if (dt < st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + st::mediaviewSaveMsgHiding) {
+				if (hidingDt >= 0 && _saveMsgOpacity.to() > 0.5) {
+					_saveMsgOpacity.start(0);
 				}
+				float64 progress = (hidingDt >= 0) ? (hidingDt / st::mediaviewSaveMsgHiding) : (dt / st::mediaviewSaveMsgShowing);
+				_saveMsgOpacity.update(qMin(progress, 1.), anim::linear);
+                if (_saveMsgOpacity.current() > 0) {
+					p.setOpacity(_saveMsgOpacity.current());
+					App::roundRect(p, _saveMsg, st::mediaviewSaveMsgBg, MediaviewSaveCorners);
+					st::mediaviewSaveMsgCheck.paint(p, _saveMsg.topLeft() + st::mediaviewSaveMsgCheckPos, width());
+
+					p.setPen(st::mediaviewSaveMsgFg);
+					p.setTextPalette(st::mediaviewTextPalette);
+					_saveMsgText.draw(p, _saveMsg.x() + st::mediaviewSaveMsgPadding.left(), _saveMsg.y() + st::mediaviewSaveMsgPadding.top(), _saveMsg.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right());
+					p.restoreTextPalette();
+					p.setOpacity(1);
+				}
+				if (!_blurred) {
+                    auto nextFrame = (dt < st::mediaviewSaveMsgShowing || hidingDt >= 0) ? int(AnimationTimerDelta) : (st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + 1 - dt);
+					_saveMsgUpdater.start(nextFrame);
+				}
+			} else {
+				_saveMsgStarted = 0;
 			}
 		}
 	} else if (_themePreviewShown) {
 		paintThemePreview(p, r);
-	} else {
+	} else if (documentBubbleShown()) {
 		if (_docRect.intersects(r)) {
 			p.fillRect(_docRect, st::mediaviewFileBg);
 			if (_docIconRect.intersects(r)) {
@@ -2273,8 +2347,8 @@ void OverlayWidget::paintEvent(QPaintEvent *e) {
 			auto o = overLevel(OverLeftNav);
 			if (o > 0) {
 				p.setOpacity(o * co);
-				for (int i = 0, l = region.rectCount(); i < l; ++i) {
-					auto fill = _leftNav.intersected(rs.at(i));
+				for (const auto &rect : rects) {
+					const auto fill = _leftNav.intersected(rect);
 					if (!fill.isEmpty()) p.fillRect(fill, st::mediaviewControlBg);
 				}
 			}
@@ -2289,8 +2363,8 @@ void OverlayWidget::paintEvent(QPaintEvent *e) {
 			auto o = overLevel(OverRightNav);
 			if (o > 0) {
 				p.setOpacity(o * co);
-				for (int i = 0, l = region.rectCount(); i < l; ++i) {
-					auto fill = _rightNav.intersected(rs.at(i));
+				for (const auto &rect : rects) {
+					const auto fill = _rightNav.intersected(rect);
 					if (!fill.isEmpty()) p.fillRect(fill, st::mediaviewControlBg);
 				}
 			}
@@ -2305,8 +2379,8 @@ void OverlayWidget::paintEvent(QPaintEvent *e) {
 			auto o = overLevel(OverClose);
 			if (o > 0) {
 				p.setOpacity(o * co);
-				for (int i = 0, l = region.rectCount(); i < l; ++i) {
-					auto fill = _closeNav.intersected(rs.at(i));
+				for (const auto &rect : rects) {
+					const auto fill = _closeNav.intersected(rect);
 					if (!fill.isEmpty()) p.fillRect(fill, st::mediaviewControlBg);
 				}
 			}
@@ -2406,14 +2480,14 @@ void OverlayWidget::paintEvent(QPaintEvent *e) {
 }
 
 void OverlayWidget::checkGroupThumbsAnimation() {
-	if (_groupThumbs && (!_gif || _gif->started())) {
+	if (_groupThumbs && (!_streamed || _streamed->player.ready())) {
 		_groupThumbs->checkForAnimationStart();
 	}
 }
 
 void OverlayWidget::paintDocRadialLoading(Painter &p, bool radial, float64 radialOpacity) {
 	float64 o = overLevel(OverIcon);
-	if (radial || (_doc && !_doc->loaded())) {
+	if (radial || (_doc && !_doc->loaded() && !_streamed)) {
 		QRect inner(QPoint(_docIconRect.x() + ((_docIconRect.width() - st::radialSize.width()) / 2), _docIconRect.y() + ((_docIconRect.height() - st::radialSize.height()) / 2)), st::radialSize);
 
 		p.setPen(Qt::NoPen);
@@ -2495,18 +2569,17 @@ void OverlayWidget::paintThemePreview(Painter &p, QRect clip) {
 
 void OverlayWidget::keyPressEvent(QKeyEvent *e) {
 	const auto ctrl = e->modifiers().testFlag(Qt::ControlModifier);
-	if (_clipController) {
+	if (_streamed) {
 		// Ctrl + F for full screen toggle is in eventFilter().
 		const auto toggleFull = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) && (e->modifiers().testFlag(Qt::AltModifier) || ctrl);
 		if (toggleFull) {
-			onVideoToggleFullScreen();
+			playbackToggleFullScreen();
 			return;
-		}
-		if (_fullScreenVideo) {
+		} else if (_fullScreenVideo) {
 			if (e->key() == Qt::Key_Escape) {
-				onVideoToggleFullScreen();
+				playbackToggleFullScreen();
 			} else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return || e->key() == Qt::Key_Space) {
-				onVideoPauseResume();
+				playbackPauseResume();
 			}
 			return;
 		}
@@ -2522,10 +2595,10 @@ void OverlayWidget::keyPressEvent(QKeyEvent *e) {
 	} else if (e->key() == Qt::Key_Copy || (e->key() == Qt::Key_C && ctrl)) {
 		onCopy();
 	} else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return || e->key() == Qt::Key_Space) {
-		if (_doc && !_doc->loading() && (fileBubbleShown() || !_doc->loaded())) {
+		if (_doc && !_doc->loading() && (documentBubbleShown() || !_doc->loaded())) {
 			onDocClick();
-		} else if (_doc && (_doc->isVideoFile() || _doc->isVideoMessage())) {
-			onVideoPauseResume();
+		} else if (_streamed) {
+			playbackPauseResume();
 		}
 	} else if (e->key() == Qt::Key_Left) {
 		if (_controlsHideTimer.isActive()) {
@@ -2587,8 +2660,11 @@ void OverlayWidget::setZoomLevel(int newZoom) {
 	if (_zoom == newZoom) return;
 
 	float64 nx, ny, z = (_zoom == ZoomToScreenLevel) ? _zoomToScreen : _zoom;
-	_w = gifShown() ? ConvertScale(_gif->width()) : (ConvertScale(_current.width()) / cIntRetinaFactor());
-	_h = gifShown() ? ConvertScale(_gif->height()) : (ConvertScale(_current.height()) / cIntRetinaFactor());
+	const auto contentSize = videoShown()
+		? ConvertScale(videoSize())
+		: ConvertScale(_current.size() / cIntRetinaFactor());
+	_w = contentSize.width();
+	_h = contentSize.height();
 	if (z >= 0) {
 		nx = (_x - width() / 2.) / (z + 1);
 		ny = (_y - height() / 2.) / (z + 1);
@@ -2733,7 +2809,7 @@ bool OverlayWidget::moveToEntity(const Entity &entity, int preloadDelta) {
 	} else {
 		setContext(std::nullopt);
 	}
-	stopGif();
+	clearStreaming();
 	if (auto photo = base::get_if<not_null<PhotoData*>>(&entity.data)) {
 		displayPhoto(*photo, entity.item);
 	} else if (auto document = base::get_if<not_null<DocumentData*>>(&entity.data)) {
@@ -2826,8 +2902,8 @@ void OverlayWidget::mouseDoubleClickEvent(QMouseEvent *e) {
 	updateOver(e->pos());
 
 	if (_over == OverVideo) {
-		onVideoToggleFullScreen();
-		onVideoPauseResume();
+		playbackToggleFullScreen();
+		playbackPauseResume();
 	} else {
 		e->ignore();
 		return OverlayParent::mouseDoubleClickEvent(e);
@@ -2968,14 +3044,14 @@ void OverlayWidget::updateOver(QPoint pos) {
 		updateOverState(OverHeader);
 	} else if (_saveVisible && _saveNav.contains(pos)) {
 		updateOverState(OverSave);
-	} else if (_doc && fileBubbleShown() && _docIconRect.contains(pos)) {
+	} else if (_doc && documentBubbleShown() && _docIconRect.contains(pos)) {
 		updateOverState(OverIcon);
 	} else if (_moreNav.contains(pos)) {
 		updateOverState(OverMore);
 	} else if (_closeNav.contains(pos)) {
 		updateOverState(OverClose);
-	} else if (_doc && fileShown() && QRect(_x, _y, _w, _h).contains(pos)) {
-		if ((_doc->isVideoFile() || _doc->isVideoMessage()) && _gif) {
+	} else if (documentContentShown() && contentRect().contains(pos)) {
+		if ((_doc->isVideoFile() || _doc->isVideoMessage()) && _streamed) {
 			updateOverState(OverVideo);
 		} else if (!_doc->loaded()) {
 			updateOverState(OverIcon);
@@ -3013,7 +3089,9 @@ void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) {
 	} else if (_over == OverClose && _down == OverClose) {
 		close();
 	} else if (_over == OverVideo && _down == OverVideo) {
-		onVideoPauseResume();
+		if (_streamed) {
+			playbackPauseResume();
+		}
 	} else if (_pressed) {
 		if (_dragging) {
 			if (_dragging > 0) {
@@ -3029,7 +3107,10 @@ void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) {
 				if (!_themePreviewRect.contains(e->pos())) {
 					close();
 				}
-			} else if (!_doc || fileShown() || !_docRect.contains(e->pos())) {
+			} else if (!_doc
+				|| documentContentShown()
+				|| !documentBubbleShown()
+				|| !_docRect.contains(e->pos())) {
 				close();
 			}
 		}
@@ -3150,8 +3231,8 @@ bool OverlayWidget::eventFilter(QObject *obj, QEvent *e) {
 	if (type == QEvent::ShortcutOverride) {
 		const auto keyEvent = static_cast<QKeyEvent*>(e);
 		const auto ctrl = keyEvent->modifiers().testFlag(Qt::ControlModifier);
-		if (keyEvent->key() == Qt::Key_F && ctrl) {
-			onVideoToggleFullScreen();
+		if (keyEvent->key() == Qt::Key_F && ctrl && _streamed) {
+			playbackToggleFullScreen();
 		}
 		return true;
 	}
@@ -3198,15 +3279,15 @@ void OverlayWidget::setVisibleHook(bool visible) {
 		// QOpenGLWidget can't properly destroy a child widget if
 		// it is hidden exactly after that, so it must be repainted
 		// before it is hidden without the child widget.
-		if (!isHidden() && _clipController) {
-			_clipController.destroy();
+		if (!isHidden() && _streamed) {
+			_streamed->controls.hide();
 			_wasRepainted = false;
 			repaint();
 			if (!_wasRepainted) {
 				// Qt has some optimization to prevent too frequent repaints.
 				// If the previous repaint was less than 1/60 second it silently
 				// converts repaint() call to an update() call. But we have to
-				// repaint right now, before hide(), with _clipController destroyed.
+				// repaint right now, before hide(), with _streamingControls destroyed.
 				auto event = QEvent(QEvent::UpdateRequest);
 				QApplication::sendEvent(this, &event);
 			}
@@ -3220,7 +3301,7 @@ void OverlayWidget::setVisibleHook(bool visible) {
 	} else {
 		QCoreApplication::instance()->removeEventFilter(this);
 
-		stopGif();
+		clearStreaming();
 		destroyThemePreview();
 		_radial.stop();
 		_current = QPixmap();
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
index 7cd9ece42..b9279f839 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_shared_media.h"
 #include "data/data_user_photos.h"
 #include "data/data_web_page.h"
+#include "media/view/media_view_playback_controls.h"
 
 namespace Ui {
 class PopupMenu;
@@ -35,9 +36,13 @@ namespace Media {
 namespace Player {
 struct TrackState;
 } // namespace Player
-namespace Clip {
-class Controller;
-} // namespace Clip
+namespace Streaming {
+struct Update;
+struct Error;
+} // namespace Streaming
+} // namespace Media
+
+namespace Media {
 namespace View {
 
 class GroupThumbs;
@@ -48,7 +53,11 @@ using OverlayParent = Ui::RpWidgetWrap<QOpenGLWidget>;
 using OverlayParent = Ui::RpWidget;
 #endif // Q_OS_MAC && !OS_MAC_OLD
 
-class OverlayWidget final : public OverlayParent, private base::Subscriber, public ClickHandlerHost {
+class OverlayWidget final
+	: public OverlayParent
+	, private base::Subscriber
+	, public ClickHandlerHost
+	, private PlaybackControls::Delegate {
 	Q_OBJECT
 
 public:
@@ -70,7 +79,6 @@ public:
 	void activateControls();
 	void onDocClick();
 
-	void clipCallback(Media::Clip::Notification notification);
 	PeerData *ui_getPeerForMouseAction();
 
 	void clearData();
@@ -105,14 +113,9 @@ private slots:
 
 	void updateImage();
 
-	void onVideoPauseResume();
-	void onVideoSeekProgress(crl::time positionMs);
-	void onVideoSeekFinished(crl::time positionMs);
-	void onVideoVolumeChanged(float64 volume);
-	void onVideoToggleFullScreen();
-	void onVideoPlayProgress(const AudioMsgId &audioId);
-
 private:
+	struct Streamed;
+
 	enum OverState {
 		OverNone,
 		OverLeftNav,
@@ -149,6 +152,16 @@ private:
 
 	void setVisibleHook(bool visible) override;
 
+	void playbackControlsPlay() override;
+	void playbackControlsPause() override;
+	void playbackControlsSeekProgress(crl::time position) override;
+	void playbackControlsSeekFinished(crl::time position) override;
+	void playbackControlsVolumeChanged(float64 volume) override;
+	void playbackControlsToFullScreen() override;
+	void playbackControlsFromFullScreen() override;
+	void playbackPauseResume();
+	void playbackToggleFullScreen();
+
 	void updateOver(QPoint mpos);
 	void moveToScreen();
 	bool moveToNext(int delta);
@@ -212,17 +225,18 @@ private:
 	void updateCursor();
 	void setZoomLevel(int newZoom);
 
-	void updateVideoPlaybackState(const Media::Player::TrackState &state);
-	void updateSilentVideoPlaybackState();
-	void restartVideoAtSeekPosition(crl::time positionMs);
-	void toggleVideoPaused();
+	void updatePlaybackState();
+	void restartAtSeekPosition(crl::time position);
+	void togglePauseResume();
 
-	void createClipController();
 	void refreshClipControllerGeometry();
 	void refreshCaptionGeometry();
 
-	void initAnimation();
-	void createClipReader();
+	void initStreaming();
+	void initStreamingThumbnail();
+	void createStreamingObjects();
+	void handleStreamingUpdate(Streaming::Update &&update);
+	void handleStreamingError(Streaming::Error &&error);
 
 	void initThemePreview();
 	void destroyThemePreview();
@@ -231,6 +245,8 @@ private:
 	void documentUpdated(DocumentData *doc);
 	void changingMsgId(not_null<HistoryItem*> row, MsgId newId);
 
+	QRect contentRect() const;
+
 	// Radial animation interface.
 	float64 radialProgress() const;
 	bool radialLoading() const;
@@ -262,6 +278,16 @@ private:
 	void validatePhotoImage(Image *image, bool blurred);
 	void validatePhotoCurrentImage();
 
+	[[nodiscard]] bool videoShown() const;
+	[[nodiscard]] QSize videoSize() const;
+	[[nodiscard]] bool videoIsGifv() const;
+	[[nodiscard]] QImage videoFrame() const;
+	[[nodiscard]] crl::time streamedPosition() const;
+	[[nodiscard]] crl::time streamedDuration() const;
+	[[nodiscard]] bool documentContentShown() const;
+	[[nodiscard]] bool documentBubbleShown() const;
+	void clearStreaming();
+
 	QBrush _transparentBrush;
 
 	PhotoData *_photo = nullptr;
@@ -285,12 +311,11 @@ private:
 	QString _dateText;
 	QString _headerText;
 
-	object_ptr<Media::Clip::Controller> _clipController = { nullptr };
 	DocumentData *_autoplayVideoDocument = nullptr;
 	bool _fullScreenVideo = false;
 	int _fullScreenZoomCache = 0;
 
-	std::unique_ptr<Media::View::GroupThumbs> _groupThumbs;
+	std::unique_ptr<GroupThumbs> _groupThumbs;
 	QRect _groupThumbsRect;
 	int _groupThumbsAvailableWidth = 0;
 	int _groupThumbsLeft = 0;
@@ -298,9 +323,8 @@ private:
 	Text _caption;
 	QRect _captionRect;
 
-	crl::time _animStarted;
-
 	int _width = 0;
+	int _height = 0;
 	int _x = 0, _y = 0, _w = 0, _h = 0;
 	int _xStart = 0, _yStart = 0;
 	int _zoom = 0; // < 0 - out, 0 - none, > 0 - in
@@ -309,21 +333,9 @@ private:
 	bool _pressed = false;
 	int32 _dragging = 0;
 	QPixmap _current;
-	Media::Clip::ReaderPointer _gif;
 	bool _blurred = true;
 
-	// Video without audio stream playback information.
-	bool _videoIsSilent = false;
-	bool _videoPaused = false;
-	bool _videoStopped = false;
-	crl::time _videoPositionMs = 0;
-	crl::time _videoDurationMs = 0;
-	int32 _videoFrequencyMs = 1000; // 1000 ms per second.
-
-	bool fileShown() const;
-	bool gifShown() const;
-	bool fileBubbleShown() const;
-	void stopGif();
+	std::unique_ptr<Streamed> _streamed;
 
 	const style::icon *_docIcon = nullptr;
 	style::color _docIconColor;
diff --git a/Telegram/SourceFiles/media/view/media_clip_controller.cpp b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp
similarity index 67%
rename from Telegram/SourceFiles/media/view/media_clip_controller.cpp
rename to Telegram/SourceFiles/media/view/media_view_playback_controls.cpp
index 6a5da7e5e..525ad40fd 100644
--- a/Telegram/SourceFiles/media/view/media_clip_controller.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp
@@ -5,10 +5,10 @@ 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 "media/view/media_clip_controller.h"
+#include "media/view/media_view_playback_controls.h"
 
 #include "media/audio/media_audio.h"
-#include "media/view/media_clip_playback.h"
+#include "media/view/media_view_playback_progress.h"
 #include "ui/widgets/labels.h"
 #include "ui/widgets/continuous_sliders.h"
 #include "ui/effects/fade_animation.h"
@@ -17,69 +17,89 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "styles/style_mediaview.h"
 
 namespace Media {
-namespace Clip {
+namespace View {
 
-Controller::Controller(QWidget *parent) : TWidget(parent)
+PlaybackControls::PlaybackControls(QWidget *parent, not_null<Delegate *> delegate)
+: RpWidget(parent)
+, _delegate(delegate)
 , _playPauseResume(this, st::mediaviewPlayButton)
 , _playbackSlider(this, st::mediaviewPlayback)
-, _playback(std::make_unique<Playback>())
+, _playbackProgress(std::make_unique<PlaybackProgress>())
 , _volumeController(this, st::mediaviewPlayback)
 , _fullScreenToggle(this, st::mediaviewFullScreenButton)
 , _playedAlready(this, st::mediaviewPlayProgressLabel)
 , _toPlayLeft(this, st::mediaviewPlayProgressLabel)
 , _fadeAnimation(std::make_unique<Ui::FadeAnimation>(this)) {
 	_fadeAnimation->show();
-	_fadeAnimation->setFinishedCallback([this] { fadeFinished(); });
-	_fadeAnimation->setUpdatedCallback([this](float64 opacity) { fadeUpdated(opacity); });
+	_fadeAnimation->setFinishedCallback([=] {
+		fadeFinished();
+	});
+	_fadeAnimation->setUpdatedCallback([=](float64 opacity) {
+		fadeUpdated(opacity);
+	});
 
 	_volumeController->setValue(Global::VideoVolume());
 	_volumeController->setChangeProgressCallback([=](float64 value) {
-		volumeChanged(value);
+		_delegate->playbackControlsVolumeChanged(value);
 	});
-	//_volumeController->setChangeFinishedCallback();
 
-	connect(_playPauseResume, SIGNAL(clicked()), this, SIGNAL(playPressed()));
-	connect(_fullScreenToggle, SIGNAL(clicked()), this, SIGNAL(toFullScreenPressed()));
-	//connect(_volumeController, SIGNAL(volumeChanged(float64)), this, SIGNAL(volumeChanged(float64)));
+	_playPauseResume->addClickHandler([=] {
+		if (_showPause) {
+			_delegate->playbackControlsPause();
+		} else {
+			_delegate->playbackControlsPlay();
+		}
+	});
+	_fullScreenToggle->addClickHandler([=] {
+		if (_inFullScreen) {
+			_delegate->playbackControlsFromFullScreen();
+		} else {
+			_delegate->playbackControlsToFullScreen();
+		}
+	});
 
-	_playback->setInLoadingStateChangedCallback([this](bool loading) {
+	_playbackProgress->setInLoadingStateChangedCallback([=](bool loading) {
 		_playbackSlider->setDisabled(loading);
 	});
-	_playback->setValueChangedCallback([this](float64 value) {
+	_playbackProgress->setValueChangedCallback([=](float64 value) {
 		_playbackSlider->setValue(value);
 	});
-	_playbackSlider->setChangeProgressCallback([this](float64 value) {
-		_playback->setValue(value, false);
-		handleSeekProgress(value); // This may destroy Controller.
+	_playbackSlider->setChangeProgressCallback([=](float64 value) {
+		_playbackProgress->setValue(value, false);
+
+		// This may destroy PlaybackControls.
+		handleSeekProgress(value);
 	});
-	_playbackSlider->setChangeFinishedCallback([this](float64 value) {
-		_playback->setValue(value, false);
+	_playbackSlider->setChangeFinishedCallback([=](float64 value) {
+		_playbackProgress->setValue(value, false);
 		handleSeekFinished(value);
 	});
 }
 
-void Controller::handleSeekProgress(float64 progress) {
+void PlaybackControls::handleSeekProgress(float64 progress) {
 	if (!_lastDurationMs) return;
 
 	auto positionMs = snap(static_cast<crl::time>(progress * _lastDurationMs), 0LL, _lastDurationMs);
 	if (_seekPositionMs != positionMs) {
 		_seekPositionMs = positionMs;
 		refreshTimeTexts();
-		emit seekProgress(positionMs); // This may destroy Controller.
+
+		// This may destroy PlaybackControls.
+		_delegate->playbackControlsSeekProgress(positionMs);
 	}
 }
 
-void Controller::handleSeekFinished(float64 progress) {
+void PlaybackControls::handleSeekFinished(float64 progress) {
 	if (!_lastDurationMs) return;
 
 	auto positionMs = snap(static_cast<crl::time>(progress * _lastDurationMs), 0LL, _lastDurationMs);
 	_seekPositionMs = -1;
-	emit seekFinished(positionMs);
+	_delegate->playbackControlsSeekFinished(positionMs);
 	refreshTimeTexts();
 }
 
 template <typename Callback>
-void Controller::startFading(Callback start) {
+void PlaybackControls::startFading(Callback start) {
 	if (!_fadeAnimation->animating()) {
 		showChildren();
 		_playbackSlider->disablePaint(true);
@@ -103,45 +123,42 @@ void Controller::startFading(Callback start) {
 	_volumeController->disablePaint(false);
 }
 
-void Controller::showAnimated() {
+void PlaybackControls::showAnimated() {
 	startFading([this]() {
 		_fadeAnimation->fadeIn(st::mediaviewShowDuration);
 	});
 }
 
-void Controller::hideAnimated() {
+void PlaybackControls::hideAnimated() {
 	startFading([this]() {
 		_fadeAnimation->fadeOut(st::mediaviewHideDuration);
 	});
 }
 
-void Controller::fadeFinished() {
+void PlaybackControls::fadeFinished() {
 	fadeUpdated(_fadeAnimation->visible() ? 1. : 0.);
 }
 
-void Controller::fadeUpdated(float64 opacity) {
+void PlaybackControls::fadeUpdated(float64 opacity) {
 	_playbackSlider->setFadeOpacity(opacity);
 	_volumeController->setFadeOpacity(opacity);
 }
 
-void Controller::updatePlayback(const Player::TrackState &state) {
+void PlaybackControls::updatePlayback(const Player::TrackState &state) {
 	updatePlayPauseResumeState(state);
-	_playback->updateState(state);
+	_playbackProgress->updateState(state);
 	updateTimeTexts(state);
 }
 
-void Controller::updatePlayPauseResumeState(const Player::TrackState &state) {
+void PlaybackControls::updatePlayPauseResumeState(const Player::TrackState &state) {
 	auto showPause = ShowPauseIcon(state.state) || (_seekPositionMs >= 0);
 	if (showPause != _showPause) {
-		disconnect(_playPauseResume, SIGNAL(clicked()), this, _showPause ? SIGNAL(pausePressed()) : SIGNAL(playPressed()));
 		_showPause = showPause;
-		connect(_playPauseResume, SIGNAL(clicked()), this, _showPause ? SIGNAL(pausePressed()) : SIGNAL(playPressed()));
-
 		_playPauseResume->setIconOverride(_showPause ? &st::mediaviewPauseIcon : nullptr, _showPause ? &st::mediaviewPauseIconOver : nullptr);
 	}
 }
 
-void Controller::updateTimeTexts(const Player::TrackState &state) {
+void PlaybackControls::updateTimeTexts(const Player::TrackState &state) {
 	qint64 position = 0, length = state.length;
 
 	if (Player::IsStoppedAtEnd(state.state)) {
@@ -166,7 +183,7 @@ void Controller::updateTimeTexts(const Player::TrackState &state) {
 	}
 }
 
-void Controller::refreshTimeTexts() {
+void PlaybackControls::refreshTimeTexts() {
 	auto alreadyChanged = false, leftChanged = false;
 	auto timeAlready = _timeAlready;
 	auto timeLeft = _timeLeft;
@@ -189,16 +206,16 @@ void Controller::refreshTimeTexts() {
 	}
 }
 
-void Controller::setInFullScreen(bool inFullScreen) {
-	_fullScreenToggle->setIconOverride(inFullScreen ? &st::mediaviewFullScreenOutIcon : nullptr, inFullScreen ? &st::mediaviewFullScreenOutIconOver : nullptr);
-	disconnect(_fullScreenToggle, SIGNAL(clicked()), this, SIGNAL(toFullScreenPressed()));
-	disconnect(_fullScreenToggle, SIGNAL(clicked()), this, SIGNAL(fromFullScreenPressed()));
-
-	auto handler = inFullScreen ? SIGNAL(fromFullScreenPressed()) : SIGNAL(toFullScreenPressed());
-	connect(_fullScreenToggle, SIGNAL(clicked()), this, handler);
+void PlaybackControls::setInFullScreen(bool inFullScreen) {
+	if (_inFullScreen != inFullScreen) {
+		_inFullScreen = inFullScreen;
+		_fullScreenToggle->setIconOverride(
+			_inFullScreen ? &st::mediaviewFullScreenOutIcon : nullptr,
+			_inFullScreen ? &st::mediaviewFullScreenOutIconOver : nullptr);
+	}
 }
 
-void Controller::resizeEvent(QResizeEvent *e) {
+void PlaybackControls::resizeEvent(QResizeEvent *e) {
 	int playTop = (height() - _playPauseResume->height()) / 2;
 	_playPauseResume->moveToLeft(st::mediaviewPlayPauseLeft, playTop);
 
@@ -216,7 +233,7 @@ void Controller::resizeEvent(QResizeEvent *e) {
 	_toPlayLeft->moveToRight(width() - (st::mediaviewPlayPauseLeft + _playPauseResume->width() + playTop) - playbackWidth, st::mediaviewPlayProgressTop);
 }
 
-void Controller::paintEvent(QPaintEvent *e) {
+void PlaybackControls::paintEvent(QPaintEvent *e) {
 	Painter p(this);
 
 	if (_fadeAnimation->paint(p)) {
@@ -231,11 +248,11 @@ void Controller::paintEvent(QPaintEvent *e) {
 	App::roundRect(p, rect(), st::mediaviewSaveMsgBg, MediaviewSaveCorners);
 }
 
-void Controller::mousePressEvent(QMouseEvent *e) {
+void PlaybackControls::mousePressEvent(QMouseEvent *e) {
 	e->accept(); // Don't pass event to the Media::View::OverlayWidget.
 }
 
-Controller::~Controller() = default;
+PlaybackControls::~PlaybackControls() = default;
 
-} // namespace Clip
+} // namespace View
 } // namespace Media
diff --git a/Telegram/SourceFiles/media/view/media_clip_controller.h b/Telegram/SourceFiles/media/view/media_view_playback_controls.h
similarity index 68%
rename from Telegram/SourceFiles/media/view/media_clip_controller.h
rename to Telegram/SourceFiles/media/view/media_view_playback_controls.h
index 009815475..94c17b24d 100644
--- a/Telegram/SourceFiles/media/view/media_clip_controller.h
+++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.h
@@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
+#include "ui/rp_widget.h"
+
 namespace Ui {
 class LabelSimple;
 class FadeAnimation;
@@ -19,15 +21,24 @@ namespace Player {
 struct TrackState;
 } // namespace Player
 
-namespace Clip {
+namespace View {
 
-class Playback;
-
-class Controller : public TWidget {
-	Q_OBJECT
+class PlaybackProgress;
 
+class PlaybackControls : public Ui::RpWidget {
 public:
-	Controller(QWidget *parent);
+	class Delegate {
+	public:
+		virtual void playbackControlsPlay() = 0;
+		virtual void playbackControlsPause() = 0;
+		virtual void playbackControlsSeekProgress(crl::time position) = 0;
+		virtual void playbackControlsSeekFinished(crl::time position) = 0;
+		virtual void playbackControlsVolumeChanged(float64 volume) = 0;
+		virtual void playbackControlsToFullScreen() = 0;
+		virtual void playbackControlsFromFullScreen() = 0;
+	};
+
+	PlaybackControls(QWidget *parent, not_null<Delegate*> delegate);
 
 	void showAnimated();
 	void hideAnimated();
@@ -35,16 +46,7 @@ public:
 	void updatePlayback(const Player::TrackState &state);
 	void setInFullScreen(bool inFullScreen);
 
-	~Controller();
-
-signals:
-	void playPressed();
-	void pausePressed();
-	void seekProgress(crl::time positionMs);
-	void seekFinished(crl::time positionMs);
-	void volumeChanged(float64 volume);
-	void toFullScreenPressed();
-	void fromFullScreenPressed();
+	~PlaybackControls();
 
 protected:
 	void resizeEvent(QResizeEvent *e) override;
@@ -64,6 +66,9 @@ private:
 	void updateTimeTexts(const Player::TrackState &state);
 	void refreshTimeTexts();
 
+	not_null<Delegate*> _delegate;
+
+	bool _inFullScreen = false;
 	bool _showPause = false;
 	bool _childrenHidden = false;
 	QString _timeAlready, _timeLeft;
@@ -72,7 +77,7 @@ private:
 
 	object_ptr<Ui::IconButton> _playPauseResume;
 	object_ptr<Ui::MediaSlider> _playbackSlider;
-	std::unique_ptr<Playback> _playback;
+	std::unique_ptr<PlaybackProgress> _playbackProgress;
 	object_ptr<Ui::MediaSlider> _volumeController;
 	object_ptr<Ui::IconButton> _fullScreenToggle;
 	object_ptr<Ui::LabelSimple> _playedAlready;
@@ -82,5 +87,5 @@ private:
 
 };
 
-} // namespace Clip
+} // namespace View
 } // namespace Media
diff --git a/Telegram/SourceFiles/media/view/media_clip_playback.cpp b/Telegram/SourceFiles/media/view/media_view_playback_progress.cpp
similarity index 80%
rename from Telegram/SourceFiles/media/view/media_clip_playback.cpp
rename to Telegram/SourceFiles/media/view/media_view_playback_progress.cpp
index e9a550ce2..57f7f025d 100644
--- a/Telegram/SourceFiles/media/view/media_clip_playback.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_playback_progress.cpp
@@ -5,23 +5,23 @@ 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 "media/view/media_clip_playback.h"
+#include "media/view/media_view_playback_progress.h"
 
 #include "media/audio/media_audio.h"
 #include "styles/style_mediaview.h"
 
 namespace Media {
-namespace Clip {
+namespace View {
 namespace {
 
 constexpr auto kPlaybackAnimationDurationMs = crl::time(200);
 
 } // namespace
 
-Playback::Playback() : _a_value(animation(this, &Playback::step_value)) {
+PlaybackProgress::PlaybackProgress() : _a_value(animation(this, &PlaybackProgress::step_value)) {
 }
 
-void Playback::updateState(const Player::TrackState &state) {
+void PlaybackProgress::updateState(const Player::TrackState &state) {
 	qint64 position = 0, length = state.length;
 
 	auto wasInLoadingState = _inLoadingState;
@@ -60,7 +60,7 @@ void Playback::updateState(const Player::TrackState &state) {
 	}
 }
 
-void Playback::updateLoadingState(float64 progress) {
+void PlaybackProgress::updateLoadingState(float64 progress) {
 	if (!_inLoadingState) {
 		_inLoadingState = true;
 		if (_inLoadingStateChanged) {
@@ -71,16 +71,16 @@ void Playback::updateLoadingState(float64 progress) {
 	setValue(progress, animated);
 }
 
-float64 Playback::value() const {
+float64 PlaybackProgress::value() const {
 	return qMin(a_value.current(), 1.);
 }
 
-float64 Playback::value(crl::time ms) {
+float64 PlaybackProgress::value(crl::time ms) {
 	_a_value.step(ms);
 	return value();
 }
 
-void Playback::setValue(float64 value, bool animated) {
+void PlaybackProgress::setValue(float64 value, bool animated) {
 	if (animated) {
 		a_value.start(value);
 		_a_value.start();
@@ -93,7 +93,7 @@ void Playback::setValue(float64 value, bool animated) {
 	}
 }
 
-void Playback::step_value(float64 ms, bool timer) {
+void PlaybackProgress::step_value(float64 ms, bool timer) {
 	auto dt = anim::Disabled() ? 1. : (ms / kPlaybackAnimationDurationMs);
 	if (dt >= 1.) {
 		_a_value.stop();
@@ -106,5 +106,5 @@ void Playback::step_value(float64 ms, bool timer) {
 	}
 }
 
-} // namespace Clip
+} // namespace View
 } // namespace Media
diff --git a/Telegram/SourceFiles/media/view/media_clip_playback.h b/Telegram/SourceFiles/media/view/media_view_playback_progress.h
similarity index 93%
rename from Telegram/SourceFiles/media/view/media_clip_playback.h
rename to Telegram/SourceFiles/media/view/media_view_playback_progress.h
index c367a837c..55b577ae5 100644
--- a/Telegram/SourceFiles/media/view/media_clip_playback.h
+++ b/Telegram/SourceFiles/media/view/media_view_playback_progress.h
@@ -14,11 +14,11 @@ namespace Player {
 struct TrackState;
 } // namespace Player
 
-namespace Clip {
+namespace View {
 
-class Playback {
+class PlaybackProgress {
 public:
-	Playback();
+	PlaybackProgress();
 
 	void setValueChangedCallback(Fn<void(float64)> callback) {
 		_valueChanged = std::move(callback);
@@ -52,5 +52,5 @@ private:
 
 };
 
-} // namespace Clip
+} // namespace View
 } // namespace Media
diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp
index 7bf16c187..2823f48c2 100644
--- a/Telegram/SourceFiles/overview/overview_layout.cpp
+++ b/Telegram/SourceFiles/overview/overview_layout.cpp
@@ -849,7 +849,6 @@ bool Voice::updateStatusText() {
 		statusSize = FileStatusSizeFailed;
 	} else if (_data->loaded()) {
 		statusSize = FileStatusSizeLoaded;
-		using State = Media::Player::State;
 		auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Voice);
 		if (state.id == AudioMsgId(_data, parent()->fullId(), state.id.playId()) && !Media::Player::IsStoppedOrStopping(state.state)) {
 			statusSize = -1 - (state.position / state.frequency);
@@ -1220,7 +1219,6 @@ bool Document::updateStatusText() {
 	} else if (_data->loaded()) {
 		if (_data->isSong()) {
 			statusSize = FileStatusSizeLoaded;
-			using State = Media::Player::State;
 			auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Song);
 			if (state.id == AudioMsgId(_data, parent()->fullId()) && !Media::Player::IsStoppedOrStopping(state.state)) {
 				statusSize = -1 - (state.position / state.frequency);
diff --git a/Telegram/SourceFiles/rpl/variable.h b/Telegram/SourceFiles/rpl/variable.h
index f318ecf7b..06f9e0e74 100644
--- a/Telegram/SourceFiles/rpl/variable.h
+++ b/Telegram/SourceFiles/rpl/variable.h
@@ -90,11 +90,11 @@ public:
 
 	template <
 		typename OtherType,
-		typename Error,
+		typename OtherError,
 		typename Generator,
 		typename = std::enable_if_t<
 			std::is_assignable_v<Type&, OtherType>>>
-	variable(producer<OtherType, Error, Generator> &&stream) {
+	variable(producer<OtherType, OtherError, Generator> &&stream) {
 		std::move(stream)
 			| start_with_next([=](auto &&data) {
 				assign(std::forward<decltype(data)>(data));
@@ -103,12 +103,12 @@ public:
 
 	template <
 		typename OtherType,
-		typename Error,
+		typename OtherError,
 		typename Generator,
 		typename = std::enable_if_t<
 			std::is_assignable_v<Type&, OtherType>>>
 	variable &operator=(
-			producer<OtherType, Error, Generator> &&stream) {
+			producer<OtherType, OtherError, Generator> &&stream) {
 		_lifetime.destroy();
 		std::move(stream)
 			| start_with_next([=](auto &&data) {
diff --git a/Telegram/SourceFiles/settings.h b/Telegram/SourceFiles/settings.h
index e3c680e7a..baee28c73 100644
--- a/Telegram/SourceFiles/settings.h
+++ b/Telegram/SourceFiles/settings.h
@@ -200,6 +200,10 @@ inline T ConvertScale(T value) {
 	return ConvertScale(value, cScale());
 }
 
+inline QSize ConvertScale(QSize size) {
+	return QSize(ConvertScale(size.width()), ConvertScale(size.height()));
+}
+
 inline void SetScaleChecked(int scale) {
 	const auto checked = (scale == kInterfaceScaleAuto)
 		? kInterfaceScaleAuto
diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt
index 457882f06..748076f57 100644
--- a/Telegram/gyp/telegram_sources.txt
+++ b/Telegram/gyp/telegram_sources.txt
@@ -472,10 +472,10 @@
 <(src_loc)/media/streaming/media_streaming_utility.h
 <(src_loc)/media/streaming/media_streaming_video_track.cpp
 <(src_loc)/media/streaming/media_streaming_video_track.h
-<(src_loc)/media/view/media_clip_controller.cpp
-<(src_loc)/media/view/media_clip_controller.h
-<(src_loc)/media/view/media_clip_playback.cpp
-<(src_loc)/media/view/media_clip_playback.h
+<(src_loc)/media/view/media_view_playback_controls.cpp
+<(src_loc)/media/view/media_view_playback_controls.h
+<(src_loc)/media/view/media_view_playback_progress.cpp
+<(src_loc)/media/view/media_view_playback_progress.h
 <(src_loc)/media/view/media_view_group_thumbs.cpp
 <(src_loc)/media/view/media_view_group_thumbs.h
 <(src_loc)/media/view/media_view_overlay_widget.cpp