diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp
index f73efed10..4d4149ce3 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp
@@ -17,6 +17,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Media {
 namespace Streaming {
 
+Instance::Instance(const Instance &other)
+: _shared(other._shared)
+, _waitingCallback(other._waitingCallback)
+, _priority(other._priority)
+, _playerLocked(other._playerLocked) {
+	if (_shared) {
+		_shared->registerInstance(this);
+		if (_playerLocked) {
+			_shared->player().lock();
+		}
+	}
+}
+
 Instance::Instance(
 	std::shared_ptr<Document> shared,
 	Fn<void()> waitingCallback)
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_instance.h b/Telegram/SourceFiles/media/streaming/media_streaming_instance.h
index 9a7cfd9c3..314779401 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_instance.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_instance.h
@@ -27,6 +27,7 @@ class Player;
 
 class Instance {
 public:
+	Instance(const Instance &other);
 	Instance(
 		std::shared_ptr<Document> shared,
 		Fn<void()> waitingCallback);
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 27113c355..d069604a6 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -345,6 +345,10 @@ struct OverlayWidget::PipWrap {
 	PipWrap(
 		QWidget *parent,
 		not_null<DocumentData*> document,
+		Data::FileOrigin origin,
+		not_null<DocumentData*> chosenQuality,
+		HistoryItem *context,
+		VideoQuality quality,
 		std::shared_ptr<Streaming::Document> shared,
 		FnMut<void()> closeAndContinue,
 		FnMut<void()> destroy);
@@ -470,6 +474,10 @@ OverlayWidget::Streamed::Streamed(
 OverlayWidget::PipWrap::PipWrap(
 	QWidget *parent,
 	not_null<DocumentData*> document,
+	Data::FileOrigin origin,
+	not_null<DocumentData*> chosenQuality,
+	HistoryItem *context,
+	VideoQuality quality,
 	std::shared_ptr<Streaming::Document> shared,
 	FnMut<void()> closeAndContinue,
 	FnMut<void()> destroy)
@@ -477,6 +485,10 @@ OverlayWidget::PipWrap::PipWrap(
 , wrapped(
 	&delegate,
 	document,
+	origin,
+	chosenQuality,
+	context,
+	quality,
 	std::move(shared),
 	std::move(closeAndContinue),
 	std::move(destroy)) {
@@ -3849,10 +3861,14 @@ bool OverlayWidget::initStreaming(const StartStreaming &startStreaming) {
 		});
 	}, _streamed->instance.lifetime());
 
+	const auto continuing = startStreaming.continueStreaming
+		&& _pip
+		&& (_pip->wrapped.shared().get()
+			== _streamed->instance.shared().get());
 	if (startStreaming.continueStreaming) {
 		_pip = nullptr;
 	}
-	if (!startStreaming.continueStreaming
+	if (!continuing
 		|| (!_streamed->instance.player().active()
 			&& !_streamed->instance.player().finished())) {
 		startStreamingPlayer(startStreaming);
@@ -4517,6 +4533,10 @@ void OverlayWidget::switchToPip() {
 	_pip = std::make_unique<PipWrap>(
 		_window,
 		document,
+		fileOrigin(),
+		_chosenQuality ? _chosenQuality : document,
+		_message,
+		_quality,
 		_streamed->instance.shared(),
 		closeAndContinue,
 		[=] { _pip = nullptr; });
diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp
index 5399ccef9..94e44cb99 100644
--- a/Telegram/SourceFiles/media/view/media_view_pip.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp
@@ -885,18 +885,30 @@ void PipPanel::updateDecorations() {
 Pip::Pip(
 	not_null<Delegate*> delegate,
 	not_null<DocumentData*> data,
+	Data::FileOrigin origin,
+	not_null<DocumentData*> chosenQuality,
+	HistoryItem *context,
+	VideoQuality quality,
 	std::shared_ptr<Streaming::Document> shared,
 	FnMut<void()> closeAndContinue,
 	FnMut<void()> destroy)
 : _delegate(delegate)
 , _data(data)
-, _instance(std::move(shared), [=] { waitingAnimationCallback(); })
+, _origin(origin)
+, _chosenQuality(chosenQuality)
+, _context(context)
+, _quality(quality)
+, _instance(
+	std::in_place,
+	std::move(shared),
+	[=] { waitingAnimationCallback(); })
 , _panel(
 	_delegate->pipParentWidget(),
 	[=](Ui::GL::Capabilities capabilities) {
 		return chooseRenderer(capabilities);
 	})
 , _playbackProgress(std::make_unique<PlaybackProgress>())
+, _dataMedia(_data->createMediaView())
 , _rotation(data->owner().mediaRotation().get(data))
 , _lastPositiveVolume((Core::App().settings().videoVolume() > 0.)
 	? Core::App().settings().videoVolume()
@@ -911,15 +923,28 @@ Pip::Pip(
 	) | rpl::start_with_next([=] {
 		_destroy();
 	}, _panel.rp()->lifetime());
+
+	if (_context) {
+		_data->owner().itemRemoved(
+		) | rpl::start_with_next([=](not_null<const HistoryItem*> data) {
+			if (_context != data) {
+				_context = nullptr;
+			}
+		}, _panel.rp()->lifetime());
+	}
 }
 
 Pip::~Pip() = default;
 
+std::shared_ptr<Streaming::Document> Pip::shared() const {
+	return _instance->shared();
+}
+
 void Pip::setupPanel() {
 	_panel.init();
 	const auto size = [&] {
-		if (!_instance.info().video.size.isEmpty()) {
-			return _instance.info().video.size;
+		if (!_instance->info().video.size.isEmpty()) {
+			return _instance->info().video.size;
 		}
 		const auto media = _data->activeMediaView();
 		if (media) {
@@ -1138,8 +1163,8 @@ void Pip::seekProgress(float64 value) {
 		_lastDurationMs);
 	if (_seekPositionMs != positionMs) {
 		_seekPositionMs = positionMs;
-		if (!_instance.player().paused()
-			&& !_instance.player().finished()) {
+		if (!_instance->player().paused()
+			&& !_instance->player().finished()) {
 			_pausedBySeek = true;
 			playbackPauseResume();
 		}
@@ -1157,7 +1182,7 @@ void Pip::seekFinish(float64 value) {
 		crl::time(0),
 		_lastDurationMs);
 	_seekPositionMs = -1;
-	_startPaused = !_pausedBySeek && !_instance.player().finished();
+	_startPaused = !_pausedBySeek && !_instance->player().finished();
 	restartAtSeekPosition(positionMs);
 }
 
@@ -1294,18 +1319,71 @@ void Pip::updatePlayPauseResumeState(const Player::TrackState &state) {
 }
 
 void Pip::setupStreaming() {
-	_instance.setPriority(kPipLoaderPriority);
-	_instance.lockPlayer();
+	_instance->setPriority(kPipLoaderPriority);
+	_instance->lockPlayer();
 
-	_instance.player().updates(
+	_instance->switchQualityRequests(
+	) | rpl::filter([=](int quality) {
+		return !_quality.manual && _quality.height != quality;
+	}) | rpl::start_with_next([=](int quality) {
+		applyVideoQuality({
+			.manual = 0,
+			.height = uint32(quality),
+		});
+	}, _instance->lifetime());
+
+	_instance->player().updates(
 	) | rpl::start_with_next_error([=](Streaming::Update &&update) {
 		handleStreamingUpdate(std::move(update));
 	}, [=](Streaming::Error &&error) {
 		handleStreamingError(std::move(error));
-	}, _instance.lifetime());
+	}, _instance->lifetime());
 	updatePlaybackState();
 }
 
+void Pip::applyVideoQuality(VideoQuality value) {
+	if (_quality == value
+		|| !_dataMedia->canBePlayed(_context)) {
+		return;
+	}
+	const auto resolved = _data->chooseQuality(_context, value);
+	if (_chosenQuality == resolved) {
+		return;
+	}
+	auto instance = Streaming::Instance(
+		resolved,
+		_data,
+		_context,
+		_origin,
+		[=] { waitingAnimationCallback(); });
+	if (!instance.valid()) {
+		return;
+	}
+
+	if (_instance->ready()) {
+		_qualityChangeFrame = currentVideoFrameImage();
+	}
+	if (!_instance->player().active()
+		|| _instance->player().finished()) {
+		_qualityChangeFinished = true;
+	}
+	_startPaused = _qualityChangeFinished || _instance->player().paused();
+
+	_quality = value;
+	Core::App().settings().setVideoQuality(value);
+	Core::App().saveSettingsDelayed();
+	_chosenQuality = resolved;
+	_instance.emplace(std::move(instance));
+	setupStreaming();
+	restartAtSeekPosition(_lastUpdatePosition);
+}
+
+QImage Pip::currentVideoFrameImage() const {
+	return _instance->player().ready()
+		? _instance->player().currentFrameImage()
+		: _instance->info().video.cover;
+}
+
 Ui::GL::ChosenRenderer Pip::chooseRenderer(
 		Ui::GL::Capabilities capabilities) {
 	const auto use = Platform::IsMac()
@@ -1336,12 +1414,12 @@ void Pip::paint(not_null<Renderer*> renderer) const {
 		.fade = controlsShown,
 		.outer = _panel.widget()->size(),
 		.rotation = _rotation,
-		.videoRotation = _instance.info().video.rotation,
+		.videoRotation = _instance->info().video.rotation,
 		.useTransparency = _panel.useTransparency(),
 	};
 	if (canUseVideoFrame()) {
 		renderer->paintTransformedVideoFrame(geometry);
-		_instance.markFrameShown();
+		_instance->markFrameShown();
 	} else {
 		const auto content = staticContent();
 		if (_preparedCoverState == ThumbState::Cover) {
@@ -1349,7 +1427,7 @@ void Pip::paint(not_null<Renderer*> renderer) const {
 		}
 		renderer->paintTransformedStaticContent(content, geometry);
 	}
-	if (_instance.waitingShown()) {
+	if (_instance->waitingShown()) {
 		renderer->paintRadialLoading(countRadialRect(), controlsShown);
 	}
 	if (controlsShown > 0) {
@@ -1535,12 +1613,14 @@ void Pip::handleStreamingUpdate(Streaming::Update &&update) {
 	v::match(update.data, [&](const Information &update) {
 		_panel.setAspectRatio(
 			FlipSizeByRotation(update.video.size, _rotation));
+		_qualityChangeFrame = QImage();
 	}, [&](PreloadedVideo) {
 		updatePlaybackState();
-	}, [&](UpdateVideo) {
+	}, [&](UpdateVideo update) {
 		_panel.update();
 		Core::App().updateNonIdle();
 		updatePlaybackState();
+		_lastUpdatePosition = update.position;
 	}, [&](PreloadedAudio) {
 		updatePlaybackState();
 	}, [&](UpdateAudio) {
@@ -1554,7 +1634,7 @@ void Pip::handleStreamingUpdate(Streaming::Update &&update) {
 }
 
 void Pip::updatePlaybackState() {
-	const auto state = _instance.player().prepareLegacyState();
+	const auto state = _instance->player().prepareLegacyState();
 	updatePlayPauseResumeState(state);
 	if (state.position == kTimeUnknown
 		|| state.length == kTimeUnknown
@@ -1619,61 +1699,65 @@ void Pip::handleStreamingError(Streaming::Error &&error) {
 }
 
 void Pip::playbackPauseResume() {
-	if (_instance.player().failed()) {
+	if (_instance->player().failed()) {
 		_panel.widget()->close();
-	} else if (_instance.player().finished()
-		|| !_instance.player().active()) {
+	} else if (_instance->player().finished()
+		|| !_instance->player().active()) {
 		_startPaused = false;
 		restartAtSeekPosition(0);
-	} else if (_instance.player().paused()) {
-		_instance.resume();
+	} else if (_instance->player().paused()) {
+		_instance->resume();
 		updatePlaybackState();
 	} else {
-		_instance.pause();
+		_instance->pause();
 		updatePlaybackState();
 	}
 }
 
 void Pip::restartAtSeekPosition(crl::time position) {
-	if (!_instance.info().video.cover.isNull()) {
+	_lastUpdatePosition = position;
+
+	if (!_instance->info().video.cover.isNull()) {
 		_preparedCoverStorage = QImage();
 		_preparedCoverState = ThumbState::Empty;
-		_instance.saveFrameToCover();
+		_instance->saveFrameToCover();
 	}
 
 	auto options = Streaming::PlaybackOptions();
 	options.position = position;
 	options.hwAllowed = Core::App().settings().hardwareAcceleratedVideo();
-	options.audioId = _instance.player().prepareLegacyState().id;
+	options.audioId = _instance->player().prepareLegacyState().id;
 	options.speed = _delegate->pipPlaybackSpeed();
 
-	_instance.play(options);
+	_instance->play(options);
 	if (_startPaused) {
-		_instance.pause();
+		_instance->pause();
 	}
 	_pausedBySeek = false;
 	updatePlaybackState();
 }
 
 bool Pip::canUseVideoFrame() const {
-	return _instance.player().ready()
-		&& !_instance.info().video.cover.isNull();
+	return _instance->player().ready()
+		&& !_instance->info().video.cover.isNull();
 }
 
 QImage Pip::videoFrame(const FrameRequest &request) const {
 	Expects(canUseVideoFrame());
 
-	return _instance.frame(request);
+	return _instance->frame(request);
 }
 
 Streaming::FrameWithInfo Pip::videoFrameWithInfo() const {
 	Expects(canUseVideoFrame());
 
-	return _instance.frameWithInfo();
+	return _instance->frameWithInfo();
 }
 
 QImage Pip::staticContent() const {
-	const auto &cover = _instance.info().video.cover;
+	const auto &cover = !_qualityChangeFrame.isNull()
+		? _qualityChangeFrame
+		: _instance->info().video.cover;
 	const auto media = _data->activeMediaView();
 	const auto use = media
 		? media
@@ -1701,7 +1785,7 @@ QImage Pip::staticContent() const {
 	}
 	_preparedCoverState = state;
 	if (state == ThumbState::Cover) {
-		_preparedCoverStorage = _instance.info().video.cover;
+		_preparedCoverStorage = cover;
 	} else {
 		_preparedCoverStorage = (good
 			? good
@@ -1727,7 +1811,7 @@ void Pip::paintRadialLoadingContent(
 		st::radialLine,
 		st::radialLine,
 		st::radialLine));
-	p.setOpacity(_instance.waitingOpacity());
+	p.setOpacity(_instance->waitingOpacity());
 	p.setPen(Qt::NoPen);
 	p.setBrush(st::radialBg);
 	{
@@ -1737,7 +1821,7 @@ void Pip::paintRadialLoadingContent(
 	p.setOpacity(1.);
 	Ui::InfiniteRadialAnimation::Draw(
 		p,
-		_instance.waitingState(),
+		_instance->waitingState(),
 		arc.topLeft(),
 		arc.size(),
 		_panel.widget()->width(),
diff --git a/Telegram/SourceFiles/media/view/media_view_pip.h b/Telegram/SourceFiles/media/view/media_view_pip.h
index d0156e1bd..503973896 100644
--- a/Telegram/SourceFiles/media/view/media_view_pip.h
+++ b/Telegram/SourceFiles/media/view/media_view_pip.h
@@ -7,13 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
+#include "data/data_file_origin.h"
 #include "media/streaming/media_streaming_instance.h"
+#include "media/media_common.h"
 #include "ui/effects/animations.h"
 #include "ui/round_rect.h"
 #include "ui/rp_widget.h"
 
 #include <QtCore/QPointer>
 
+class HistoryItem;
+
 namespace base {
 class PowerSaveBlocker;
 } // namespace base
@@ -32,12 +36,15 @@ struct Capabilities;
 } // namespace GL
 } // namespace Ui
 
-namespace Media {
-namespace Player {
+namespace Media::Player {
 struct TrackState;
-} // namespace Player
+} // namespace Media::Player
 
-namespace View {
+namespace Media::Streaming {
+class Document;
+} // namespace Media::Streaming
+
+namespace Media::View {
 
 class PlaybackProgress;
 
@@ -134,11 +141,17 @@ public:
 	Pip(
 		not_null<Delegate*> delegate,
 		not_null<DocumentData*> data,
+		Data::FileOrigin origin,
+		not_null<DocumentData*> chosenQuality,
+		HistoryItem *context,
+		VideoQuality quality,
 		std::shared_ptr<Streaming::Document> shared,
 		FnMut<void()> closeAndContinue,
 		FnMut<void()> destroy);
 	~Pip();
 
+	[[nodiscard]] std::shared_ptr<Streaming::Document> shared() const;
+
 private:
 	enum class OverState {
 		None,
@@ -245,6 +258,8 @@ private:
 		QRect outer,
 		float64 shown) const;
 	[[nodiscard]] QRect countRadialRect() const;
+	void applyVideoQuality(VideoQuality value);
+	[[nodiscard]] QImage currentVideoFrameImage() const;
 
 	void seekUpdate(QPoint position);
 	void seekProgress(float64 value);
@@ -252,7 +267,11 @@ private:
 
 	const not_null<Delegate*> _delegate;
 	const not_null<DocumentData*> _data;
-	Streaming::Instance _instance;
+	const Data::FileOrigin _origin;
+	DocumentData *_chosenQuality = nullptr;
+	HistoryItem *_context = nullptr;
+	Media::VideoQuality _quality;
+	std::optional<Streaming::Instance> _instance;
 	bool _opengl = false;
 	PipPanel _panel;
 	QSize _size;
@@ -260,6 +279,10 @@ private:
 	std::unique_ptr<PlaybackProgress> _playbackProgress;
 	std::shared_ptr<Data::DocumentMedia> _dataMedia;
 
+	QImage _qualityChangeFrame;
+	bool _qualityChangeFinished = false;
+	crl::time _lastUpdatePosition = 0;
+
 	bool _showPause = false;
 	bool _startPaused = false;
 	bool _pausedBySeek = false;
@@ -288,5 +311,4 @@ private:
 
 };
 
-} // namespace View
-} // namespace Media
+} // namespace Media::View