diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 3b9d28d95..d1c6825b0 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1492,6 +1492,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_player_message_today" = "Today at {time}";
 "lng_player_message_yesterday" = "Yesterday at {time}";
 "lng_player_message_date" = "{date} at {time}";
+"lng_player_cant_play" = "This file can't be played in Telegram Desktop.\n\nWould you like to download it and open it in an external player?";
+"lng_player_download" = "Download";
 
 "lng_rights_edit_admin" = "Manage permissions";
 "lng_rights_edit_admin_header" = "What can this admin do?";
diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp
index 8e99d9daa..dff775907 100644
--- a/Telegram/SourceFiles/data/data_document.cpp
+++ b/Telegram/SourceFiles/data/data_document.cpp
@@ -300,6 +300,9 @@ void DocumentOpenClickHandler::Open(
 		Core::App().showDocument(data, context);
 		location.accessDisable();
 		return;
+	} else if (data->inappPlaybackFailed()) {
+		::Data::HandleUnsupportedMedia(data, msgId);
+		return;
 	} else if (data->canBePlayed()) {
 		if (data->isAudioFile() || data->isVoiceMessage()) {
 			Media::Player::instance()->playPause({ data, msgId });
@@ -705,55 +708,32 @@ void DocumentData::performActionOnLoad() {
 		return;
 	}
 
-	auto loc = location(true);
-	auto already = loc.name();
-	auto item = _actionOnLoadMsgId.msg ? App::histItemById(_actionOnLoadMsgId) : nullptr;
-	auto showImage = !isVideoFile() && (size < App::kImageSizeLimit);
-	auto playVoice = isVoiceMessage() && (_actionOnLoad == ActionOnLoadPlayInline || _actionOnLoad == ActionOnLoadOpen);
-	auto playMusic = isAudioFile() && (_actionOnLoad == ActionOnLoadPlayInline || _actionOnLoad == ActionOnLoadOpen);
-	auto playAnimation = isAnimation()
-		&& (_actionOnLoad == ActionOnLoadPlayInline || _actionOnLoad == ActionOnLoadOpen)
+	const auto loc = location(true);
+	const auto &already = loc.name();
+	const auto item = _actionOnLoadMsgId.msg
+		? App::histItemById(_actionOnLoadMsgId)
+		: nullptr;
+	const auto showImage = !isVideoFile() && (size < App::kImageSizeLimit);
+	const auto playVoice = isVoiceMessage();
+	const auto playMusic = isAudioFile();
+	const auto playAnimation = isAnimation()
+		&& loaded()
 		&& showImage
 		&& item;
-	if (auto applyTheme = isTheme()) {
+	if (isTheme()) {
 		if (!loc.isEmpty() && loc.accessEnable()) {
 			Core::App().showDocument(this, item);
 			loc.accessDisable();
-			return;
 		}
-	}
-	if (playVoice || playMusic) {
-		DocumentOpenClickHandler::Open({}, this, item, ActionOnLoadNone);
-	} else if (playAnimation) {
-		if (loaded()) {
-			if (_actionOnLoad == ActionOnLoadPlayInline && item) {
-				_owner->requestAnimationPlayInline(item);
-			} else {
-				Core::App().showDocument(this, item);
-			}
-		}
-	} else {
-		if (already.isEmpty()) return;
-
-		if (_actionOnLoad == ActionOnLoadOpenWith) {
+	} else if (_actionOnLoad == ActionOnLoadOpenWith) {
+		if (!already.isEmpty()) {
 			File::OpenWith(already, QCursor::pos());
-		} else if (_actionOnLoad == ActionOnLoadOpen || _actionOnLoad == ActionOnLoadPlayInline) {
-			if (isVoiceMessage() || isAudioFile() || isVideoFile()) {
-				if (Data::IsValidMediaFile(already)) {
-					File::Launch(already);
-				}
-				_owner->markMediaRead(this);
-			} else if (loc.accessEnable()) {
-				if (showImage && QImageReader(loc.name()).canRead()) {
-					Core::App().showDocument(this, item);
-				} else {
-					LaunchWithWarning(already, item);
-				}
-				loc.accessDisable();
-			} else {
-				LaunchWithWarning(already, item);
-			}
 		}
+	} else if (playVoice
+		|| playMusic
+		|| playAnimation
+		|| !already.isEmpty()) {
+		DocumentOpenClickHandler::Open({}, this, item, _actionOnLoad);
 	}
 	_actionOnLoad = ActionOnLoadNone;
 }
@@ -1222,6 +1202,14 @@ bool DocumentData::canBePlayed() const {
 		&& (loaded() || canBeStreamed());
 }
 
+void DocumentData::setInappPlaybackFailed() {
+	_inappPlaybackFailed = true;
+}
+
+bool DocumentData::inappPlaybackFailed() const {
+	return _inappPlaybackFailed;
+}
+
 auto DocumentData::createStreamingLoader(Data::FileOrigin origin) const
 -> std::unique_ptr<Media::Streaming::Loader> {
 	// #TODO streaming create local file loader
@@ -1596,4 +1584,28 @@ base::binary_guard ReadImageAsync(
 	return std::move(right);
 }
 
+void HandleUnsupportedMedia(
+		not_null<DocumentData*> document,
+		FullMsgId contextId) {
+	document->setInappPlaybackFailed();
+	auto filepath = document->filepath(
+		DocumentData::FilePathResolveSaveFromData);
+	if (filepath.isEmpty()) {
+		const auto save = [=] {
+			Ui::hideLayer();
+			DocumentSaveClickHandler::Save(
+				(contextId ? contextId : Data::FileOrigin()),
+				document,
+				App::histItemById(contextId));
+		};
+		Ui::show(Box<ConfirmBox>(
+			lang(lng_player_cant_play),
+			lang(lng_player_download),
+			lang(lng_cancel),
+			save));
+	} else if (IsValidMediaFile(filepath)) {
+		File::Launch(filepath);
+	}
+}
+
 } // namespace Data
diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h
index a4828490a..2c284bfc9 100644
--- a/Telegram/SourceFiles/data/data_document.h
+++ b/Telegram/SourceFiles/data/data_document.h
@@ -228,6 +228,9 @@ public:
 	[[nodiscard]] auto createStreamingLoader(Data::FileOrigin origin) const
 		-> std::unique_ptr<Media::Streaming::Loader>;
 
+	void setInappPlaybackFailed();
+	[[nodiscard]] bool inappPlaybackFailed() const;
+
 	~DocumentData();
 
 	DocumentId id = 0;
@@ -272,6 +275,7 @@ private:
 	int32 _duration = -1;
 	bool _isImage = false;
 	bool _supportsStreaming = false;
+	bool _inappPlaybackFailed = false;
 
 	ActionOnLoad _actionOnLoad = ActionOnLoadNone;
 	FullMsgId _actionOnLoadMsgId;
@@ -393,4 +397,8 @@ base::binary_guard ReadImageAsync(
 	FnMut<QImage(QImage)> postprocess,
 	FnMut<void(QImage&&)> done);
 
+void HandleUnsupportedMedia(
+	not_null<DocumentData*> document,
+	FullMsgId contextId);
+
 } // namespace Data
diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp
index e0924effc..db1d3f681 100644
--- a/Telegram/SourceFiles/mainwidget.cpp
+++ b/Telegram/SourceFiles/mainwidget.cpp
@@ -389,11 +389,9 @@ MainWidget::MainWidget(
 	connect(_dialogs, SIGNAL(cancelled()), this, SLOT(dialogsCancelled()));
 	connect(this, SIGNAL(dialogsUpdated()), _dialogs, SLOT(onListScroll()));
 	connect(_history, SIGNAL(cancelled()), _dialogs, SLOT(activate()));
-	subscribe(Media::Player::Updated(), [this](const AudioMsgId &audioId) {
-		if (audioId.type() != AudioMsgId::Type::Video) {
-			handleAudioUpdate(audioId);
-		}
-	});
+	subscribe(
+		Media::Player::instance()->updatedNotifier(),
+		[=](const Media::Player::TrackState &state) { handleAudioUpdate(state); });
 	subscribe(session().calls().currentCallChanged(), [this](Calls::Call *call) { setCurrentCall(call); });
 
 	session().data().currentExportView(
@@ -1173,29 +1171,17 @@ void MainWidget::messagesAffected(
 	}
 }
 
-void MainWidget::handleAudioUpdate(const AudioMsgId &audioId) {
+void MainWidget::handleAudioUpdate(const Media::Player::TrackState &state) {
 	using State = Media::Player::State;
-	const auto document = audioId.audio();
-	auto state = Media::Player::instance()->getState(audioId.type());
-	if (state.id == audioId && state.state == State::StoppedAtStart) {
-		state.state = State::Stopped;
-		Media::Player::mixer()->clearStoppedAtStart(audioId);
-
-		auto filepath = document->filepath(DocumentData::FilePathResolveSaveFromData);
-		if (!filepath.isEmpty()) {
-			if (Data::IsValidMediaFile(filepath)) {
-				File::Launch(filepath);
-			}
-		}
+	const auto document = state.id.audio();
+	if (!Media::Player::IsStoppedOrStopping(state.state)) {
+		createPlayer();
+	} else if (state.state == State::StoppedAtStart) {
+		Data::HandleUnsupportedMedia(document, state.id.contextId());
+		closeBothPlayers();
 	}
 
-	if (state.id == audioId) {
-		if (!Media::Player::IsStoppedOrStopping(state.state)) {
-			createPlayer();
-		}
-	}
-
-	if (const auto item = App::histItemById(audioId.contextId())) {
+	if (const auto item = App::histItemById(state.id.contextId())) {
 		session().data().requestItemRepaint(item);
 	}
 	if (const auto items = InlineBots::Layout::documentItems()) {
diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h
index 60f4c5c2f..4ad0db805 100644
--- a/Telegram/SourceFiles/mainwidget.h
+++ b/Telegram/SourceFiles/mainwidget.h
@@ -42,6 +42,7 @@ namespace Player {
 class Widget;
 class VolumeWidget;
 class Panel;
+struct TrackState;
 } // namespace Player
 } // namespace Media
 
@@ -350,7 +351,7 @@ private:
 	void animationCallback();
 	void handleAdaptiveLayoutUpdate();
 	void updateWindowAdaptiveLayout();
-	void handleAudioUpdate(const AudioMsgId &audioId);
+	void handleAudioUpdate(const Media::Player::TrackState &state);
 	void updateMediaPlayerPosition();
 	void updateMediaPlaylistPosition(int x);
 	void updateControlsGeometry();
diff --git a/Telegram/SourceFiles/media/audio/media_audio.cpp b/Telegram/SourceFiles/media/audio/media_audio.cpp
index d53aceee1..905eb7c98 100644
--- a/Telegram/SourceFiles/media/audio/media_audio.cpp
+++ b/Telegram/SourceFiles/media/audio/media_audio.cpp
@@ -1247,14 +1247,6 @@ void Mixer::setStoppedState(Track *current, State state) {
 	}
 }
 
-void Mixer::clearStoppedAtStart(const AudioMsgId &audio) {
-	QMutexLocker lock(&AudioMutex);
-	auto track = trackForType(audio.type());
-	if (track && track->state.id == audio && track->state.state == State::StoppedAtStart) {
-		setStoppedState(track);
-	}
-}
-
 // Thread: Main. Must be locked: AudioMutex.
 void Mixer::detachTracks() {
 	for (auto i = 0; i != kTogetherLimit; ++i) {
diff --git a/Telegram/SourceFiles/media/audio/media_audio.h b/Telegram/SourceFiles/media/audio/media_audio.h
index 9aec4ce90..0c95fd644 100644
--- a/Telegram/SourceFiles/media/audio/media_audio.h
+++ b/Telegram/SourceFiles/media/audio/media_audio.h
@@ -153,8 +153,6 @@ public:
 
 	TrackState currentState(AudioMsgId::Type type);
 
-	void clearStoppedAtStart(const AudioMsgId &audio);
-
 	// Thread: Main. Must be locked: AudioMutex.
 	void detachTracks();
 
diff --git a/Telegram/SourceFiles/media/player/media_player_instance.cpp b/Telegram/SourceFiles/media/player/media_player_instance.cpp
index 3cbd6f571..59ffa5a05 100644
--- a/Telegram/SourceFiles/media/player/media_player_instance.cpp
+++ b/Telegram/SourceFiles/media/player/media_player_instance.cpp
@@ -327,7 +327,9 @@ void Instance::play(AudioMsgId::Type type) {
 			if (IsStopped(state.state)) {
 				play(state.id);
 			} else if (data->streamed) {
-				data->streamed->player.resume();
+				if (data->streamed->player.active()) {
+					data->streamed->player.resume();
+				}
 				emitUpdate(type);
 			} else {
 				mixer()->resume(state.id);
@@ -414,7 +416,9 @@ Streaming::PlaybackOptions Instance::streamingOptions(
 void Instance::pause(AudioMsgId::Type type) {
 	if (const auto data = getData(type)) {
 		if (data->streamed) {
-			data->streamed->player.pause();
+			if (data->streamed->player.active()) {
+				data->streamed->player.pause();
+			}
 			emitUpdate(type);
 		} else {
 			const auto state = getState(type);
@@ -442,12 +446,9 @@ void Instance::stop(AudioMsgId::Type type) {
 void Instance::playPause(AudioMsgId::Type type) {
 	if (const auto data = getData(type)) {
 		if (data->streamed) {
-			if (data->streamed->player.finished()
-				|| data->streamed->player.failed()) {
-				auto options = Streaming::PlaybackOptions();
-				options.mode = Streaming::Mode::Audio;
-				options.audioId = data->streamed->id;
-				data->streamed->player.play(options);
+			if (!data->streamed->player.active()) {
+				data->streamed->player.play(
+					streamingOptions(data->streamed->id));
 			} else if (data->streamed->player.paused()) {
 				data->streamed->player.resume();
 			} else {
@@ -702,12 +703,19 @@ void Instance::handleStreamingUpdate(
 		};
 		finishTrack(data->streamed->info.audio.state);
 		emitUpdate(data->type);
+		if (data->streamed && data->streamed->player.finished()) {
+			data->streamed = nullptr;
+		}
 	});
 }
 
 void Instance::handleStreamingError(
 		not_null<Data*> data,
 		Streaming::Error &&error) {
+	emitUpdate(data->type);
+	if (data->streamed && data->streamed->player.failed()) {
+		data->streamed = nullptr;
+	}
 }
 
 } // namespace Player
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp
index e8f8977c3..e38acf98d 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp
@@ -98,7 +98,7 @@ void File::Context::logFatal(QLatin1String method, AvErrorWrap error) {
 Stream File::Context::initStream(AVMediaType type) {
 	auto result = Stream();
 	const auto index = result.index = av_find_best_stream(
-		_formatContext,
+		_format.get(),
 		type,
 		-1,
 		-1,
@@ -108,7 +108,7 @@ Stream File::Context::initStream(AVMediaType type) {
 		return {};
 	}
 
-	const auto info = _formatContext->streams[index];
+	const auto info = _format->streams[index];
 	if (type == AVMEDIA_TYPE_VIDEO) {
 		result.rotation = ReadRotationFromMetadata(info);
 	} else if (type == AVMEDIA_TYPE_AUDIO) {
@@ -130,7 +130,7 @@ Stream File::Context::initStream(AVMediaType type) {
 	result.timeBase = info->time_base;
 	result.duration = (info->duration != AV_NOPTS_VALUE)
 		? PtsToTimeCeil(info->duration, result.timeBase)
-		: PtsToTimeCeil(_formatContext->duration, kUniversalTimeBase);
+		: PtsToTimeCeil(_format->duration, kUniversalTimeBase);
 	if (result.duration == kTimeUnknown || !result.duration) {
 		return {};
 	}
@@ -151,7 +151,7 @@ void File::Context::seekToPosition(
 	//
 	//const auto seekFlags = 0;
 	//error = av_seek_frame(
-	//	_formatContext,
+	//	_format,
 	//	streamIndex,
 	//	TimeToPts(position, kUniversalTimeBase),
 	//	seekFlags);
@@ -160,7 +160,7 @@ void File::Context::seekToPosition(
 	//}
 	//
 	error = av_seek_frame(
-		_formatContext,
+		_format.get(),
 		stream.index,
 		TimeToPts(
 			std::clamp(position, crl::time(0), stream.duration - 1),
@@ -176,7 +176,7 @@ base::variant<Packet, AvErrorWrap> File::Context::readPacket() {
 	auto error = AvErrorWrap();
 
 	auto result = Packet();
-	error = av_read_frame(_formatContext, &result.fields());
+	error = av_read_frame(_format.get(), &result.fields());
 	if (unroll()) {
 		return AvErrorWrap();
 	} else if (!error) {
@@ -193,37 +193,16 @@ void File::Context::start(crl::time position) {
 	if (unroll()) {
 		return;
 	}
-	_ioBuffer = reinterpret_cast<uchar*>(av_malloc(AVBlockSize));
-	_ioContext = avio_alloc_context(
-		_ioBuffer,
-		AVBlockSize,
-		0,
-		static_cast<void*>(this),
+	_format = MakeFormatPointer(
+		static_cast<void *>(this),
 		&Context::Read,
 		nullptr,
 		&Context::Seek);
-	_formatContext = avformat_alloc_context();
-	if (!_formatContext) {
-		return logFatal(qstr("avformat_alloc_context"));
+	if (!_format) {
+		return fail();
 	}
-	_formatContext->pb = _ioContext;
 
-	auto options = (AVDictionary*)nullptr;
-	const auto guard = gsl::finally([&] { av_dict_free(&options); });
-	av_dict_set(&options, "usetoc", "1", 0);
-	error = avformat_open_input(
-		&_formatContext,
-		nullptr,
-		nullptr,
-		&options);
-	if (error) {
-		_ioBuffer = nullptr;
-		return logFatal(qstr("avformat_open_input"), error);
-	}
-	_opened = true;
-	_formatContext->flags |= AVFMT_FLAG_FAST_SEEK;
-
-	if ((error = avformat_find_stream_info(_formatContext, nullptr))) {
+	if ((error = avformat_find_stream_info(_format.get(), nullptr))) {
 		return logFatal(qstr("avformat_find_stream_info"), error);
 	}
 
@@ -299,20 +278,7 @@ void File::Context::fail() {
 	_delegate->fileError();
 }
 
-File::Context::~Context() {
-	if (_opened) {
-		avformat_close_input(&_formatContext);
-	}
-	if (_ioContext) {
-		av_freep(&_ioContext->buffer);
-		av_freep(&_ioContext);
-	} else if (_ioBuffer) {
-		av_freep(&_ioBuffer);
-	}
-	if (_formatContext) {
-		avformat_free_context(_formatContext);
-	}
-}
+File::Context::~Context() = default;
 
 bool File::Context::finished() const {
 	// #TODO streaming later looping
@@ -348,6 +314,7 @@ void File::stop() {
 		_context->interrupt();
 		_thread.join();
 	}
+	_reader.stop();
 	_context.reset();
 }
 
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_file.h b/Telegram/SourceFiles/media/streaming/media_streaming_file.h
index ab590c004..6fc71874c 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_file.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_file.h
@@ -87,9 +87,7 @@ private:
 		crl::semaphore _semaphore;
 		std::atomic<bool> _interrupted = false;
 
-		uchar *_ioBuffer = nullptr;
-		AVIOContext *_ioContext = nullptr;
-		AVFormatContext *_formatContext = nullptr;
+		FormatPointer _format;
 
 	};
 
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
index 4137ae880..2af53981c 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
@@ -355,7 +355,7 @@ void Player::provideStartInformation() {
 void Player::fail() {
 	_sessionLifetime = rpl::lifetime();
 	const auto stopGuarded = crl::guard(&_sessionGuard, [=] { stop(); });
-	_stage = Stage::Failed;
+	_lastFailureStage = _stage;
 	_updates.fire_error({});
 	stopGuarded();
 }
@@ -364,6 +364,7 @@ void Player::play(const PlaybackOptions &options) {
 	Expects(options.speed >= 0.5 && options.speed <= 2.);
 
 	stop();
+	_lastFailureStage = Stage::Uninitialized;
 
 	_options = options;
 	if (!Media::Audio::SupportsSpeedControl()) {
@@ -374,14 +375,14 @@ void Player::play(const PlaybackOptions &options) {
 }
 
 void Player::pause() {
-	Expects(valid());
+	Expects(active());
 
 	_pausedByUser = true;
 	updatePausedState();
 }
 
 void Player::resume() {
-	Expects(valid());
+	Expects(active());
 
 	_pausedByUser = false;
 	updatePausedState();
@@ -510,9 +511,7 @@ void Player::start() {
 void Player::stop() {
 	_file->stop();
 	_sessionLifetime = rpl::lifetime();
-	if (_stage != Stage::Failed) {
-		_stage = Stage::Uninitialized;
-	}
+	_stage = Stage::Uninitialized;
 	_audio = nullptr;
 	_video = nullptr;
 	invalidate_weak_ptrs(&_sessionGuard);
@@ -526,11 +525,14 @@ void Player::stop() {
 }
 
 bool Player::failed() const {
-	return (_stage == Stage::Failed);
+	return (_lastFailureStage != Stage::Uninitialized);
 }
 
 bool Player::playing() const {
-	return (_stage == Stage::Started) && !_paused && !finished();
+	return (_stage == Stage::Started)
+		&& !paused()
+		&& !finished()
+		&& !failed();
 }
 
 bool Player::buffering() const {
@@ -548,7 +550,7 @@ bool Player::finished() const {
 }
 
 void Player::setSpeed(float64 speed) {
-	Expects(valid());
+	Expects(active());
 	Expects(speed >= 0.5 && speed <= 2.);
 
 	if (!Media::Audio::SupportsSpeedControl()) {
@@ -565,12 +567,12 @@ void Player::setSpeed(float64 speed) {
 	}
 }
 
-bool Player::valid() const {
-	return (_stage != Stage::Uninitialized) && (_stage != Stage::Failed);
+bool Player::active() const {
+	return (_stage != Stage::Uninitialized) && !finished() && !failed();
 }
 
 bool Player::ready() const {
-	return valid() && (_stage != Stage::Initializing);
+	return (_stage != Stage::Uninitialized) && (_stage != Stage::Initializing);
 }
 
 rpl::producer<Update, Error> Player::updates() const {
@@ -588,7 +590,11 @@ Media::Player::TrackState Player::prepareLegacyState() const {
 
 	auto result = Media::Player::TrackState();
 	result.id = _audioId.externalPlayId() ? _audioId : _options.audioId;
-	result.state = finished()
+	result.state = (_lastFailureStage == Stage::Started)
+		? State::StoppedAtError
+		: failed()
+		? State::StoppedAtStart
+		: finished()
 		? State::StoppedAtEnd
 		: paused()
 		? State::Paused
@@ -609,6 +615,8 @@ Media::Player::TrackState Player::prepareLegacyState() const {
 			: document->duration();
 		if (duration > 0) {
 			result.length = duration * crl::time(1000);
+		} else {
+			result.length = std::max(result.position, crl::time(0));
 		}
 	}
 	result.frequency = kMsFrequency;
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.h b/Telegram/SourceFiles/media/streaming/media_streaming_player.h
index 6c3c3c2e1..1720e8134 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_player.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.h
@@ -44,10 +44,10 @@ public:
 	void resume();
 	void stop();
 
-	bool valid() const;
-	bool ready() const;
+	[[nodiscard]] bool active() const;
+	[[nodiscard]] bool ready() const;
 
-	float64 speed() const;
+	[[nodiscard]] float64 speed() const;
 	void setSpeed(float64 speed); // 0.5 <= speed <= 2.
 
 	[[nodiscard]] bool playing() const;
@@ -72,7 +72,6 @@ private:
 		Initializing,
 		Ready,
 		Started,
-		Failed
 	};
 
 	// Thread-safe.
@@ -143,6 +142,7 @@ private:
 	// Belongs to the main thread.
 	Information _information;
 	Stage _stage = Stage::Uninitialized;
+	Stage _lastFailureStage = Stage::Uninitialized;
 	bool _pausedByUser = false;
 	bool _pausedByWaitingForData = false;
 	bool _paused = false;
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp
index fc9db6a5a..3ce075bf9 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp
@@ -540,6 +540,10 @@ Reader::Reader(
 	}
 }
 
+void Reader::stop() {
+	_waiting = nullptr;
+}
+
 std::shared_ptr<Reader::CacheHelper> Reader::InitCacheHelper(
 		std::optional<Storage::Cache::Key> baseKey) {
 	if (!baseKey) {
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_reader.h b/Telegram/SourceFiles/media/streaming/media_streaming_reader.h
index eac09d044..b37e59d06 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_reader.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_reader.h
@@ -39,6 +39,8 @@ public:
 
 	void headerDone();
 
+	void stop();
+
 	~Reader();
 
 private:
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp
index d84fa4398..3d046f2f7 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp
@@ -22,6 +22,7 @@ constexpr auto kSkipInvalidDataPackets = 10;
 constexpr auto kAlignImageBy = 16;
 constexpr auto kPixelBytesSize = 4;
 constexpr auto kImageFormat = QImage::Format_ARGB32_Premultiplied;
+constexpr auto kAvioBlockSize = 4096;
 
 void AlignedImageBufferCleanupHandler(void* data) {
 	const auto buffer = static_cast<uchar*>(data);
@@ -68,6 +69,82 @@ QImage CreateFrameStorage(QSize size) {
 		cleanupData);
 }
 
+IOPointer MakeIOPointer(
+		void *opaque,
+		int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
+		int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
+		int64_t(*seek)(void *opaque, int64_t offset, int whence)) {
+	auto buffer = reinterpret_cast<uchar*>(av_malloc(kAvioBlockSize));
+	if (!buffer) {
+		LogError(qstr("av_malloc"));
+		return {};
+	}
+	auto result = IOPointer(avio_alloc_context(
+		buffer,
+		kAvioBlockSize,
+		write ? 1 : 0,
+		opaque,
+		read,
+		write,
+		seek));
+	if (!result) {
+		av_freep(&buffer);
+		LogError(qstr("avio_alloc_context"));
+		return {};
+	}
+	return result;
+}
+
+void IODeleter::operator()(AVIOContext *value) {
+	if (value) {
+		av_freep(&value->buffer);
+		avio_context_free(&value);
+	}
+}
+
+FormatPointer MakeFormatPointer(
+		void *opaque,
+		int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
+		int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
+		int64_t(*seek)(void *opaque, int64_t offset, int whence)) {
+	auto io = MakeIOPointer(opaque, read, write, seek);
+	if (!io) {
+		return {};
+	}
+	auto result = avformat_alloc_context();
+	if (!result) {
+		LogError(qstr("avformat_alloc_context"));
+		return {};
+	}
+	result->pb = io.get();
+
+	auto options = (AVDictionary*)nullptr;
+	const auto guard = gsl::finally([&] { av_dict_free(&options); });
+	av_dict_set(&options, "usetoc", "1", 0);
+	const auto error = AvErrorWrap(avformat_open_input(
+		&result,
+		nullptr,
+		nullptr,
+		&options));
+	if (error) {
+		// avformat_open_input freed 'result' in case an error happened.
+		LogError(qstr("avformat_open_input"), error);
+		return {};
+	}
+	result->flags |= AVFMT_FLAG_FAST_SEEK;
+
+	// Now FormatPointer will own and free the IO context.
+	io.release();
+	return FormatPointer(result);
+}
+
+void FormatDeleter::operator()(AVFormatContext *value) {
+	if (value) {
+		const auto deleter = IOPointer(value->pb);
+		avformat_close_input(&value);
+	}
+}
+
 CodecPointer MakeCodecPointer(not_null<AVStream*> stream) {
 	auto error = AvErrorWrap();
 
@@ -120,10 +197,10 @@ void FrameDeleter::operator()(AVFrame *value) {
 	av_frame_free(&value);
 }
 
-SwsContextPointer MakeSwsContextPointer(
+SwscalePointer MakeSwscalePointer(
 		not_null<AVFrame*> frame,
 		QSize resize,
-		SwsContextPointer *existing) {
+		SwscalePointer *existing) {
 	// We have to use custom caching for SwsContext, because
 	// sws_getCachedContext checks passed flags with existing context flags,
 	// and re-creates context if they're different, but in the process of
@@ -139,7 +216,7 @@ SwsContextPointer MakeSwsContextPointer(
 	}
 	if (frame->format <= AV_PIX_FMT_NONE || frame->format >= AV_PIX_FMT_NB) {
 		LogError(qstr("frame->format"));
-		return SwsContextPointer();
+		return SwscalePointer();
 	}
 
 	const auto result = sws_getCachedContext(
@@ -157,12 +234,12 @@ SwsContextPointer MakeSwsContextPointer(
 	if (!result) {
 		LogError(qstr("sws_getCachedContext"));
 	}
-	return SwsContextPointer(
+	return SwscalePointer(
 		result,
 		{ resize, QSize{ frame->width, frame->height }, frame->format });
 }
 
-void SwsContextDeleter::operator()(SwsContext *value) {
+void SwscaleDeleter::operator()(SwsContext *value) {
 	if (value) {
 		sws_freeContext(value);
 	}
@@ -341,11 +418,11 @@ QImage ConvertFrame(
 			from += deltaFrom;
 		}
 	} else {
-		stream.swsContext = MakeSwsContextPointer(
+		stream.swscale = MakeSwscalePointer(
 			frame,
 			resize,
-			&stream.swsContext);
-		if (!stream.swsContext) {
+			&stream.swscale);
+		if (!stream.swscale) {
 			return QImage();
 		}
 
@@ -354,7 +431,7 @@ QImage ConvertFrame(
 		int linesize[AV_NUM_DATA_POINTERS] = { storage.bytesPerLine(), 0 };
 
 		const auto lines = sws_scale(
-			stream.swsContext.get(),
+			stream.swscale.get(),
 			frame->data,
 			frame->linesize,
 			0,
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_utility.h b/Telegram/SourceFiles/media/streaming/media_streaming_utility.h
index 76aebb0eb..6b0707171 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_utility.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_utility.h
@@ -115,6 +115,26 @@ private:
 
 };
 
+struct IODeleter {
+	void operator()(AVIOContext *value);
+};
+using IOPointer = std::unique_ptr<AVIOContext, IODeleter>;
+[[nodiscard]] IOPointer MakeIOPointer(
+	void *opaque,
+	int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
+	int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
+	int64_t(*seek)(void *opaque, int64_t offset, int whence));
+
+struct FormatDeleter {
+	void operator()(AVFormatContext *value);
+};
+using FormatPointer = std::unique_ptr<AVFormatContext, FormatDeleter>;
+[[nodiscard]] FormatPointer MakeFormatPointer(
+	void *opaque,
+	int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
+	int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
+	int64_t(*seek)(void *opaque, int64_t offset, int whence));
+
 struct CodecDeleter {
 	void operator()(AVCodecContext *value);
 };
@@ -129,18 +149,18 @@ using FramePointer = std::unique_ptr<AVFrame, FrameDeleter>;
 [[nodiscard]] bool FrameHasData(AVFrame *frame);
 void ClearFrameMemory(AVFrame *frame);
 
-struct SwsContextDeleter {
+struct SwscaleDeleter {
 	QSize resize;
 	QSize frameSize;
 	int frameFormat = int(AV_PIX_FMT_NONE);
 
 	void operator()(SwsContext *value);
 };
-using SwsContextPointer = std::unique_ptr<SwsContext, SwsContextDeleter>;
-[[nodiscard]] SwsContextPointer MakeSwsContextPointer(
+using SwscalePointer = std::unique_ptr<SwsContext, SwscaleDeleter>;
+[[nodiscard]] SwscalePointer MakeSwscalePointer(
 	not_null<AVFrame*> frame,
 	QSize resize,
-	SwsContextPointer *existing = nullptr);
+	SwscalePointer *existing = nullptr);
 
 struct Stream {
 	int index = -1;
@@ -156,7 +176,7 @@ struct Stream {
 
 	// Video only.
 	int rotation = 0;
-	SwsContextPointer swsContext;
+	SwscalePointer swscale;
 };
 
 void LogError(QLatin1String method);
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index dd475f83f..bcb8a479b 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -1925,9 +1925,12 @@ void OverlayWidget::streamingReady(Streaming::Information &&info) {
 	validateStreamedGoodThumbnail();
 	if (videoShown()) {
 		const auto contentSize = ConvertScale(videoSize());
-		_w = contentSize.width();
-		_h = contentSize.height();
-		contentSizeChanged();
+		if (_w != contentSize.width() || _h != contentSize.height()) {
+			update(contentRect());
+			_w = contentSize.width();
+			_h = contentSize.height();
+			contentSizeChanged();
+		}
 	}
 	this->update(contentRect());
 	playbackWaitingChange(false);
@@ -2014,6 +2017,7 @@ void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) {
 
 void OverlayWidget::handleStreamingError(Streaming::Error &&error) {
 	playbackWaitingChange(false);
+	updatePlaybackState();
 }
 
 void OverlayWidget::playbackWaitingChange(bool waiting) {
@@ -2124,7 +2128,13 @@ void OverlayWidget::refreshClipControllerGeometry() {
 }
 
 void OverlayWidget::playbackControlsPlay() {
-	playbackPauseResume();
+	const auto legacy = _streamed->player.prepareLegacyState();
+	if (legacy.state == Player::State::StoppedAtStart) {
+		Data::HandleUnsupportedMedia(_doc, _msgid);
+		close();
+	} else {
+		playbackPauseResume();
+	}
 }
 
 void OverlayWidget::playbackControlsPause() {