From b73f1be856f1d7bf0ce748a19a46e07f9d7fc369 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 30 Jul 2019 16:57:39 +0200
Subject: [PATCH] Make some video player code reusable.

---
 Telegram/CMakeLists.txt                       |   2 +
 .../streaming/media_streaming_document.cpp    | 255 ++++++++++++++++++
 .../streaming/media_streaming_document.h      |  84 ++++++
 .../media/view/media_view_overlay_widget.cpp  | 202 ++++----------
 .../media/view/media_view_overlay_widget.h    |   2 -
 .../SourceFiles/media/view/mediaview.style    |   5 -
 Telegram/gyp/telegram/sources.txt             |   2 +
 Telegram/lib_ui                               |   2 +-
 8 files changed, 398 insertions(+), 156 deletions(-)
 create mode 100644 Telegram/SourceFiles/media/streaming/media_streaming_document.cpp
 create mode 100644 Telegram/SourceFiles/media/streaming/media_streaming_document.h

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 390011b00..98f543538 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -617,6 +617,8 @@ PRIVATE
     media/streaming/media_streaming_audio_track.cpp
     media/streaming/media_streaming_audio_track.h
     media/streaming/media_streaming_common.h
+    media/streaming/media_streaming_document.cpp
+    media/streaming/media_streaming_document.h
     media/streaming/media_streaming_file.cpp
     media/streaming/media_streaming_file.h
     media/streaming/media_streaming_file_delegate.h
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp
new file mode 100644
index 000000000..e967f363e
--- /dev/null
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp
@@ -0,0 +1,255 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "media/streaming/media_streaming_document.h"
+
+#include "data/data_session.h"
+#include "data/data_document.h"
+#include "data/data_file_origin.h"
+#include "storage/file_download.h" // Storage::kMaxFileInMemory.
+#include "styles/style_widgets.h"
+
+#include <QtCore/QBuffer>
+
+namespace Media {
+namespace Streaming {
+namespace {
+
+constexpr auto kWaitingFastDuration = crl::time(200);
+constexpr auto kWaitingShowDuration = crl::time(500);
+constexpr auto kWaitingShowDelay = crl::time(500);
+constexpr auto kGoodThumbnailQuality = 87;
+
+} // namespace
+
+void Instance::setWaitingCallback(Fn<void()> callback) {
+	_waitingCallback = std::move(callback);
+}
+
+void Instance::callWaitingCallback() {
+	if (_waitingCallback) {
+		_waitingCallback();
+	}
+}
+
+Document::Document(
+	not_null<DocumentData*> document,
+	Data::FileOrigin origin)
+: _player(
+	&document->owner(),
+	document->owner().documentStreamedReader(document, origin))
+, _radial(
+	[=] { waitingCallback(); },
+	st::defaultInfiniteRadialAnimation)
+, _document(document) {
+	_player.updates(
+	) | rpl::start_with_next_error([=](Update &&update) {
+		handleUpdate(std::move(update));
+	}, [=](Streaming::Error &&error) {
+		handleError(std::move(error));
+	}, lifetime());
+
+	_player.fullInCache(
+	) | rpl::start_with_next([=](bool fullInCache) {
+		_document->setLoadedInMediaCache(fullInCache);
+	}, lifetime());
+}
+
+const Player &Document::player() const {
+	return _player;
+}
+
+const Information &Document::info() const {
+	return _info;
+}
+
+void Document::play(const PlaybackOptions &options) {
+	_player.play(options);
+	_info.audio.state.position
+		= _info.video.state.position
+		= options.position;
+	waitingChange(true);
+}
+
+void Document::pause() {
+	_player.pause();
+}
+
+void Document::resume() {
+	_player.resume();
+}
+
+void Document::saveFrameToCover() {
+	auto request = Streaming::FrameRequest();
+	//request.radius = (_doc && _doc->isVideoMessage())
+	//	? ImageRoundRadius::Ellipse
+	//	: ImageRoundRadius::None;
+	_info.video.cover = _player.ready()
+		? _player.frame(request)
+		: _info.video.cover;
+}
+
+not_null<Instance*> Document::addInstance() {
+	return _instances.emplace(std::make_unique<Instance>()).first->get();
+}
+
+void Document::removeInstance(not_null<Instance*> instance) {
+	const auto i = ranges::lower_bound(
+		_instances,
+		instance.get(),
+		ranges::less(),
+		&std::unique_ptr<Instance>::get);
+	if (i != _instances.end() && i->get() == instance) {
+		_instances.erase(i);
+	}
+}
+
+bool Document::waitingShown() const {
+	if (!_fading.animating() && !_waiting) {
+		_radial.stop(anim::type::instant);
+		return false;
+	}
+	return _radial.animating();
+}
+
+float64 Document::waitingOpacity() const {
+	return _fading.value(_waiting ? 1. : 0.);
+}
+
+Ui::RadialState Document::waitingState() const {
+	return _radial.computeState();
+}
+
+rpl::lifetime &Document::lifetime() {
+	return _player.lifetime();
+}
+
+void Document::handleUpdate(Update &&update) {
+	update.data.match([&](Information &update) {
+		ready(std::move(update));
+	}, [&](const PreloadedVideo &update) {
+		_info.video.state.receivedTill = update.till;
+	}, [&](const UpdateVideo &update) {
+		_info.video.state.position = update.position;
+	}, [&](const PreloadedAudio &update) {
+		_info.audio.state.receivedTill = update.till;
+	}, [&](const UpdateAudio &update) {
+		_info.audio.state.position = update.position;
+	}, [&](const WaitingForData &update) {
+		waitingChange(update.waiting);
+	}, [&](MutedByOther) {
+	}, [&](Finished) {
+		const auto finishTrack = [](TrackState &state) {
+			state.position = state.receivedTill = state.duration;
+		};
+		finishTrack(_info.audio.state);
+		finishTrack(_info.video.state);
+	});
+}
+
+void Document::handleError(Error &&error) {
+	if (error == Error::NotStreamable) {
+		_document->setNotSupportsStreaming();
+	} else if (error == Error::OpenFailed) {
+		_document->setInappPlaybackFailed();
+	}
+	waitingChange(false);
+}
+
+void Document::ready(Information &&info) {
+	_info = std::move(info);
+	validateGoodThumbnail();
+	waitingChange(false);
+}
+
+void Document::waitingChange(bool waiting) {
+	if (_waiting == waiting) {
+		return;
+	}
+	_waiting = waiting;
+	const auto fade = [=](crl::time duration) {
+		if (!_radial.animating()) {
+			_radial.start(
+				st::defaultInfiniteRadialAnimation.sineDuration);
+		}
+		_fading.start(
+			[=] { waitingCallback(); },
+			_waiting ? 0. : 1.,
+			_waiting ? 1. : 0.,
+			duration);
+	};
+	if (waiting) {
+		if (_radial.animating()) {
+			_timer.cancel();
+			fade(kWaitingFastDuration);
+		} else {
+			_timer.callOnce(kWaitingShowDelay);
+			_timer.setCallback([=] {
+				fade(kWaitingShowDuration);
+			});
+		}
+	} else {
+		_timer.cancel();
+		if (_radial.animating()) {
+			fade(kWaitingFastDuration);
+		}
+	}
+}
+
+void Document::validateGoodThumbnail() {
+	const auto good = _document->goodThumbnail();
+	if (_info.video.cover.isNull()
+		|| (good && good->loaded())
+		|| _document->uploading()) {
+		return;
+	}
+	auto image = [&] {
+		auto result = _info.video.cover;
+		if (_info.video.rotation != 0) {
+			auto transform = QTransform();
+			transform.rotate(_info.video.rotation);
+			result = result.transformed(transform);
+		}
+		if (result.size() != _info.video.size) {
+			result = result.scaled(
+				_info.video.size,
+				Qt::IgnoreAspectRatio,
+				Qt::SmoothTransformation);
+		}
+		return result;
+	}();
+
+	auto bytes = QByteArray();
+	{
+		auto buffer = QBuffer(&bytes);
+		image.save(&buffer, "JPG", kGoodThumbnailQuality);
+	}
+	const auto length = bytes.size();
+	if (!length || length > Storage::kMaxFileInMemory) {
+		LOG(("App Error: Bad thumbnail data for saving to cache."));
+	} else if (_document->uploading()) {
+		_document->setGoodThumbnailOnUpload(
+			std::move(image),
+			std::move(bytes));
+	} else {
+		_document->owner().cache().putIfEmpty(
+			_document->goodThumbnailCacheKey(),
+			Storage::Cache::Database::TaggedValue(
+				std::move(bytes),
+				Data::kImageCacheTag));
+		_document->refreshGoodThumbnail();
+	}
+}
+
+void Document::waitingCallback() {
+	for (const auto &instance : _instances) {
+		instance->callWaitingCallback();
+	}
+}
+
+} // namespace Streaming
+} // namespace Media
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_document.h b/Telegram/SourceFiles/media/streaming/media_streaming_document.h
new file mode 100644
index 000000000..8e5febb0c
--- /dev/null
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_document.h
@@ -0,0 +1,84 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "media/streaming/media_streaming_player.h"
+#include "ui/effects/radial_animation.h"
+#include "ui/effects/animations.h"
+#include "base/timer.h"
+
+class DocumentData;
+
+namespace Data {
+struct FileOrigin;
+} // namespace Data
+
+namespace Media {
+namespace Streaming {
+
+class Instance {
+public:
+	void setWaitingCallback(Fn<void()> callback);
+
+	void callWaitingCallback();
+
+private:
+	Fn<void()> _waitingCallback;
+
+};
+
+class Document {
+public:
+	Document(
+		not_null<DocumentData*> document,
+		Data::FileOrigin origin);
+
+	[[nodiscard]] const Player &player() const;
+	[[nodiscard]] const Information &info() const;
+
+	void play(const PlaybackOptions &options);
+	void pause();
+	void resume();
+	void saveFrameToCover();
+
+	[[nodiscard]] not_null<Instance*> addInstance();
+	void removeInstance(not_null<Instance*> instance);
+
+	[[nodiscard]] bool waitingShown() const;
+	[[nodiscard]] float64 waitingOpacity() const;
+	[[nodiscard]] Ui::RadialState waitingState() const;
+
+	[[nodiscard]] rpl::lifetime &lifetime();
+
+private:
+	void waitingCallback();
+
+	void handleUpdate(Update &&update);
+	void handleError(Error &&error);
+
+	void ready(Information &&info);
+	void waitingChange(bool waiting);
+
+	void validateGoodThumbnail();
+
+	Player _player;
+	Information _info;
+
+	bool _waiting = false;
+	mutable Ui::InfiniteRadialAnimation _radial;
+	Ui::Animations::Simple _fading;
+	base::Timer _timer;
+	base::flat_set<std::unique_ptr<Instance>> _instances;
+
+	not_null<DocumentData*> _document;
+
+};
+
+
+} // namespace Streaming
+} // namespace Media
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index a168f0082..5bab7bd67 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/view/media_view_group_thumbs.h"
 #include "media/streaming/media_streaming_player.h"
 #include "media/streaming/media_streaming_reader.h"
+#include "media/streaming/media_streaming_document.h"
 #include "media/player/media_player_instance.h"
 #include "history/history.h"
 #include "history/history_message.h"
@@ -66,10 +67,6 @@ namespace Media {
 namespace View {
 namespace {
 
-constexpr auto kGoodThumbnailQuality = 87;
-constexpr auto kWaitingFastDuration = crl::time(200);
-constexpr auto kWaitingShowDuration = crl::time(500);
-constexpr auto kWaitingShowDelay = crl::time(500);
 constexpr auto kPreloadCount = 4;
 
 // macOS OpenGL renderer fails to render larger texture
@@ -188,20 +185,16 @@ struct OverlayWidget::Collage {
 struct OverlayWidget::Streamed {
 	template <typename Callback>
 	Streamed(
-		not_null<Data::Session*> owner,
-		std::shared_ptr<Streaming::Reader> reader,
+		not_null<DocumentData*> document,
+		Data::FileOrigin origin,
 		QWidget *controlsParent,
 		not_null<PlaybackControls::Delegate*> controlsDelegate,
 		Callback &&loadingCallback);
 
-	Streaming::Player player;
-	Streaming::Information info;
+	Streaming::Document document;
+	not_null<Streaming::Instance*> instance;
 	PlaybackControls controls;
 
-	bool waiting = false;
-	Ui::InfiniteRadialAnimation radial;
-	Ui::Animations::Simple fading;
-	base::Timer timer;
 	QImage frameForDirectPaint;
 
 	bool withSound = false;
@@ -211,16 +204,15 @@ struct OverlayWidget::Streamed {
 
 template <typename Callback>
 OverlayWidget::Streamed::Streamed(
-	not_null<Data::Session*> owner,
-	std::shared_ptr<Streaming::Reader> reader,
+	not_null<DocumentData*> document,
+	Data::FileOrigin origin,
 	QWidget *controlsParent,
 	not_null<PlaybackControls::Delegate*> controlsDelegate,
 	Callback &&loadingCallback)
-: player(owner, std::move(reader))
-, controls(controlsParent, controlsDelegate)
-, radial(
-	std::forward<Callback>(loadingCallback),
-	st::mediaviewStreamingRadial) {
+: document(document, origin)
+, instance(this->document.addInstance())
+, controls(controlsParent, controlsDelegate) {
+	instance->setWaitingCallback(std::forward<Callback>(loadingCallback));
 }
 
 OverlayWidget::OverlayWidget()
@@ -368,13 +360,13 @@ void OverlayWidget::moveToScreen(bool force) {
 }
 
 bool OverlayWidget::videoShown() const {
-	return _streamed && !_streamed->info.video.cover.isNull();
+	return _streamed && !_streamed->document.info().video.cover.isNull();
 }
 
 QSize OverlayWidget::videoSize() const {
 	Expects(videoShown());
 
-	return _streamed->info.video.size;
+	return _streamed->document.info().video.size;
 }
 
 bool OverlayWidget::videoIsGifv() const {
@@ -388,9 +380,9 @@ QImage OverlayWidget::videoFrame() const {
 	//request.radius = (_doc && _doc->isVideoMessage())
 	//	? ImageRoundRadius::Ellipse
 	//	: ImageRoundRadius::None;
-	return _streamed->player.ready()
-		? _streamed->player.frame(request)
-		: _streamed->info.video.cover;
+	return _streamed->document.player().ready()
+		? _streamed->document.player().frame(request)
+		: _streamed->document.info().video.cover;
 }
 
 QImage OverlayWidget::videoFrameForDirectPaint() const {
@@ -2008,17 +2000,12 @@ void OverlayWidget::initStreaming() {
 
 	Core::App().updateNonIdle();
 
-	_streamed->player.updates(
+	_streamed->document.player().updates(
 	) | rpl::start_with_next_error([=](Streaming::Update &&update) {
 		handleStreamingUpdate(std::move(update));
 	}, [=](Streaming::Error &&error) {
 		handleStreamingError(std::move(error));
-	}, _streamed->player.lifetime());
-
-	_streamed->player.fullInCache(
-	) | rpl::start_with_next([=](bool fullInCache) {
-		_doc->setLoadedInMediaCache(fullInCache);
-	}, _streamed->player.lifetime());
+	}, _streamed->document.lifetime());
 
 	restartAtSeekPosition(0);
 }
@@ -2063,8 +2050,6 @@ void OverlayWidget::initStreamingThumbnail() {
 }
 
 void OverlayWidget::streamingReady(Streaming::Information &&info) {
-	_streamed->info = std::move(info);
-	validateStreamedGoodThumbnail();
 	if (videoShown()) {
 		const auto contentSize = style::ConvertScale(videoSize());
 		if (contentSize != QSize(_width, _height)) {
@@ -2075,13 +2060,12 @@ void OverlayWidget::streamingReady(Streaming::Information &&info) {
 		}
 	}
 	this->update(contentRect());
-	playbackWaitingChange(false);
 }
 
 void OverlayWidget::createStreamingObjects() {
 	_streamed = std::make_unique<Streamed>(
-		&_doc->owner(),
-		_doc->owner().documentStreamedReader(_doc, fileOrigin()),
+		_doc,
+		fileOrigin(),
 		this,
 		static_cast<PlaybackControls::Delegate*>(this),
 		[=] { waitingAnimationCallback(); });
@@ -2101,79 +2085,38 @@ void OverlayWidget::createStreamingObjects() {
 QImage OverlayWidget::transformVideoFrame(QImage frame) const {
 	Expects(videoShown());
 
-	if (_streamed->info.video.rotation != 0) {
+	if (_streamed->document.info().video.rotation != 0) {
 		auto transform = QTransform();
-		transform.rotate(_streamed->info.video.rotation);
+		transform.rotate(_streamed->document.info().video.rotation);
 		frame = frame.transformed(transform);
 	}
-	if (frame.size() != _streamed->info.video.size) {
+	if (frame.size() != _streamed->document.info().video.size) {
 		frame = frame.scaled(
-			_streamed->info.video.size,
+			_streamed->document.info().video.size,
 			Qt::IgnoreAspectRatio,
 			Qt::SmoothTransformation);
 	}
 	return frame;
 }
 
-void OverlayWidget::validateStreamedGoodThumbnail() {
-	Expects(_streamed != nullptr);
-	Expects(_doc != nullptr);
-
-	const auto good = _doc->goodThumbnail();
-	if (!videoShown() || (good && good->loaded()) || _doc->uploading()) {
-		return;
-	}
-	auto image = transformVideoFrame(_streamed->info.video.cover);
-	auto bytes = QByteArray();
-	{
-		auto buffer = QBuffer(&bytes);
-		image.save(&buffer, "JPG", kGoodThumbnailQuality);
-	}
-	const auto length = bytes.size();
-	if (!length || length > Storage::kMaxFileInMemory) {
-		LOG(("App Error: Bad thumbnail data for saving to cache."));
-	} else if (_doc->uploading()) {
-		_doc->setGoodThumbnailOnUpload(
-			std::move(image),
-			std::move(bytes));
-	} else {
-		_doc->owner().cache().putIfEmpty(
-			_doc->goodThumbnailCacheKey(),
-			Storage::Cache::Database::TaggedValue(
-				std::move(bytes),
-				Data::kImageCacheTag));
-		_doc->refreshGoodThumbnail();
-	}
-}
-
 void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) {
 	using namespace Streaming;
 
 	update.data.match([&](Information &update) {
 		streamingReady(std::move(update));
 	}, [&](const PreloadedVideo &update) {
-		_streamed->info.video.state.receivedTill = update.till;
 		updatePlaybackState();
 	}, [&](const UpdateVideo &update) {
-		_streamed->info.video.state.position = update.position;
 		this->update(contentRect());
 		Core::App().updateNonIdle();
 		updatePlaybackState();
 	}, [&](const PreloadedAudio &update) {
-		_streamed->info.audio.state.receivedTill = update.till;
 		updatePlaybackState();
 	}, [&](const UpdateAudio &update) {
-		_streamed->info.audio.state.position = update.position;
 		updatePlaybackState();
-	}, [&](const WaitingForData &update) {
-		playbackWaitingChange(update.waiting);
+	}, [&](WaitingForData) {
 	}, [&](MutedByOther) {
 	}, [&](Finished) {
-		const auto finishTrack = [](Streaming::TrackState &state) {
-			state.position = state.receivedTill = state.duration;
-		};
-		finishTrack(_streamed->info.audio.state);
-		finishTrack(_streamed->info.video.state);
 		updatePlaybackState();
 	});
 }
@@ -2187,47 +2130,10 @@ void OverlayWidget::handleStreamingError(Streaming::Error &&error) {
 	if (!_doc->canBePlayed()) {
 		redisplayContent();
 	} else {
-		playbackWaitingChange(false);
 		updatePlaybackState();
 	}
 }
 
-void OverlayWidget::playbackWaitingChange(bool waiting) {
-	Expects(_streamed != nullptr);
-
-	if (_streamed->waiting == waiting) {
-		return;
-	}
-	_streamed->waiting = waiting;
-	const auto fade = [=](crl::time duration) {
-		if (!_streamed->radial.animating()) {
-			_streamed->radial.start(
-				st::defaultInfiniteRadialAnimation.sineDuration);
-		}
-		_streamed->fading.start(
-			[=] { update(radialRect()); },
-			_streamed->waiting ? 0. : 1.,
-			_streamed->waiting ? 1. : 0.,
-			duration);
-	};
-	if (waiting) {
-		if (_streamed->radial.animating()) {
-			_streamed->timer.cancel();
-			fade(kWaitingFastDuration);
-		} else {
-			_streamed->timer.callOnce(kWaitingShowDelay);
-			_streamed->timer.setCallback([=] {
-				fade(kWaitingShowDuration);
-			});
-		}
-	} else {
-		_streamed->timer.cancel();
-		if (_streamed->radial.animating()) {
-			fade(kWaitingFastDuration);
-		}
-	}
-}
-
 void OverlayWidget::initThemePreview() {
 	using namespace Window::Theme;
 
@@ -2363,18 +2269,18 @@ void OverlayWidget::playbackPauseResume() {
 	Expects(_streamed != nullptr);
 
 	_streamed->resumeOnCallEnd = false;
-	if (_streamed->player.failed()) {
+	if (_streamed->document.player().failed()) {
 		clearStreaming();
 		initStreaming();
-	} else if (_streamed->player.finished()) {
+	} else if (_streamed->document.player().finished()) {
 		_streamingStartPaused = false;
 		restartAtSeekPosition(0);
-	} else if (_streamed->player.paused()) {
-		_streamed->player.resume();
+	} else if (_streamed->document.player().paused()) {
+		_streamed->document.resume();
 		updatePlaybackState();
 		playbackPauseMusic();
 	} else {
-		_streamed->player.pause();
+		_streamed->document.pause();
 		updatePlaybackState();
 	}
 }
@@ -2384,7 +2290,7 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) {
 	Expects(_doc != nullptr);
 
 	if (videoShown()) {
-		_streamed->info.video.cover = videoFrame();
+		_streamed->document.saveFrameToCover();
 		_current = Images::PixmapFast(transformVideoFrame(videoFrame()));
 		update(contentRect());
 	}
@@ -2395,25 +2301,22 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) {
 		options.mode = Streaming::Mode::Video;
 		options.loop = true;
 	}
-	_streamed->player.play(options);
+	_streamed->document.play(options);
 	if (_streamingStartPaused) {
-		_streamed->player.pause();
+		_streamed->document.pause();
 	} else {
 		playbackPauseMusic();
 	}
 	_streamed->pausedBySeek = false;
 
-	_streamed->info.audio.state.position
-		= _streamed->info.video.state.position
-		= position;
 	updatePlaybackState();
-	playbackWaitingChange(true);
 }
 
 void OverlayWidget::playbackControlsSeekProgress(crl::time position) {
 	Expects(_streamed != nullptr);
 
-	if (!_streamed->player.paused() && !_streamed->player.finished()) {
+	if (!_streamed->document.player().paused()
+		&& !_streamed->document.player().finished()) {
 		_streamed->pausedBySeek = true;
 		playbackControlsPause();
 	}
@@ -2423,7 +2326,7 @@ void OverlayWidget::playbackControlsSeekFinished(crl::time position) {
 	Expects(_streamed != nullptr);
 
 	_streamingStartPaused = !_streamed->pausedBySeek
-		&& !_streamed->player.finished();
+		&& !_streamed->document.player().finished();
 	restartAtSeekPosition(position);
 }
 
@@ -2461,11 +2364,12 @@ void OverlayWidget::playbackToggleFullScreen() {
 void OverlayWidget::playbackPauseOnCall() {
 	Expects(_streamed != nullptr);
 
-	if (_streamed->player.finished() || _streamed->player.paused()) {
+	if (_streamed->document.player().finished()
+		|| _streamed->document.player().paused()) {
 		return;
 	}
 	_streamed->resumeOnCallEnd = true;
-	_streamed->player.pause();
+	_streamed->document.pause();
 	updatePlaybackState();
 }
 
@@ -2474,7 +2378,7 @@ void OverlayWidget::playbackResumeOnCall() {
 
 	if (_streamed->resumeOnCallEnd) {
 		_streamed->resumeOnCallEnd = false;
-		_streamed->player.resume();
+		_streamed->document.resume();
 		updatePlaybackState();
 		playbackPauseMusic();
 	}
@@ -2496,7 +2400,7 @@ void OverlayWidget::updatePlaybackState() {
 	if (videoIsGifv()) {
 		return;
 	}
-	const auto state = _streamed->player.prepareLegacyState();
+	const auto state = _streamed->document.player().prepareLegacyState();
 	if (state.position != kTimeUnknown && state.length != kTimeUnknown) {
 		_streamed->controls.updatePlayback(state);
 	}
@@ -2799,7 +2703,8 @@ void OverlayWidget::paintEvent(QPaintEvent *e) {
 }
 
 void OverlayWidget::checkGroupThumbsAnimation() {
-	if (_groupThumbs && (!_streamed || _streamed->player.ready())) {
+	if (_groupThumbs
+		&& (!_streamed || _streamed->document.player().ready())) {
 		_groupThumbs->checkForAnimationStart();
 	}
 }
@@ -2811,7 +2716,7 @@ void OverlayWidget::paintTransformedVideoFrame(Painter &p) {
 	//	const auto fill = rect.intersected(this->rect());
 	//	PaintImageProfile(p, image, rect, fill);
 	//} else {
-	const auto rotation = _streamed->info.video.rotation;
+	const auto rotation = _streamed->document.info().video.rotation;
 	const auto rotated = [](QRect rect, int rotation) {
 		switch (rotation) {
 		case 0: return rect;
@@ -2851,13 +2756,7 @@ void OverlayWidget::paintRadialLoading(
 		bool radial,
 		float64 radialOpacity) {
 	if (_streamed) {
-		if (!_streamed->radial.animating()) {
-			return;
-		}
-		if (!_streamed->fading.animating() && !_streamed->waiting) {
-			if (!_streamed->waiting) {
-				_streamed->radial.stop(anim::type::instant);
-			}
+		if (!_streamed->document.waitingShown()) {
 			return;
 		}
 	} else if (!radial && (!_doc || _doc->loaded())) {
@@ -2910,9 +2809,16 @@ void OverlayWidget::paintRadialLoadingContent(
 
 	if (_streamed) {
 		paintBg(
-			_streamed->fading.value(_streamed->waiting ? 1. : 0.),
+			_streamed->document.waitingOpacity(),
 			st::radialBg);
-		_streamed->radial.draw(p, arc.topLeft(), arc.size(), width());
+		Ui::InfiniteRadialAnimation::Draw(
+			p,
+			_streamed->document.waitingState(),
+			arc.topLeft(),
+			arc.size(),
+			width(),
+			st::radialFg,
+			st::radialLine);
 		return;
 	}
 	if (_photo) {
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
index 7c3152a13..95d2710c6 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
@@ -178,7 +178,6 @@ private:
 	void playbackPauseOnCall();
 	void playbackResumeOnCall();
 	void playbackPauseMusic();
-	void playbackWaitingChange(bool waiting);
 
 	void updateOver(QPoint mpos);
 	void moveToScreen(bool force = false);
@@ -265,7 +264,6 @@ private:
 	void createStreamingObjects();
 	void handleStreamingUpdate(Streaming::Update &&update);
 	void handleStreamingError(Streaming::Error &&error);
-	void validateStreamedGoodThumbnail();
 
 	void initThemePreview();
 	void destroyThemePreview();
diff --git a/Telegram/SourceFiles/media/view/mediaview.style b/Telegram/SourceFiles/media/view/mediaview.style
index 0f0a2c8cf..f02e0c957 100644
--- a/Telegram/SourceFiles/media/view/mediaview.style
+++ b/Telegram/SourceFiles/media/view/mediaview.style
@@ -188,11 +188,6 @@ mediaviewGroupWidthMax: 160px;
 mediaviewGroupSkip: 3px;
 mediaviewGroupSkipCurrent: 12px;
 
-mediaviewStreamingRadial: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
-	color: radialFg;
-	thickness: radialLine;
-}
-
 themePreviewSize: size(903px, 584px);
 themePreviewBg: windowBg;
 themePreviewOverlayOpacity: 0.8;
diff --git a/Telegram/gyp/telegram/sources.txt b/Telegram/gyp/telegram/sources.txt
index 2fbc09079..70df322f1 100644
--- a/Telegram/gyp/telegram/sources.txt
+++ b/Telegram/gyp/telegram/sources.txt
@@ -512,6 +512,8 @@
 <(src_loc)/media/streaming/media_streaming_audio_track.cpp
 <(src_loc)/media/streaming/media_streaming_audio_track.h
 <(src_loc)/media/streaming/media_streaming_common.h
+<(src_loc)/media/streaming/media_streaming_document.cpp
+<(src_loc)/media/streaming/media_streaming_document.h
 <(src_loc)/media/streaming/media_streaming_file.cpp
 <(src_loc)/media/streaming/media_streaming_file.h
 <(src_loc)/media/streaming/media_streaming_file_delegate.h
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 604f62599..21b976569 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 604f62599e9ee5f4d57d778ebbe9403c2a875245
+Subproject commit 21b976569ae2051955c7346295c7029a75bf1bbc