From da358615e040cb706c42a6c5097a8b26f8579d79 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 25 Oct 2018 12:12:56 +0400 Subject: [PATCH] Generate high quality video thumbnail when loaded. --- Telegram/SourceFiles/data/data_document.cpp | 29 +++ Telegram/SourceFiles/data/data_document.h | 7 + .../data/data_document_good_thumbnail.cpp | 217 ++++++++++++++++++ .../data/data_document_good_thumbnail.h | 75 ++++++ Telegram/SourceFiles/data/data_types.cpp | 10 + Telegram/SourceFiles/data/data_types.h | 1 + .../history/history_media_types.cpp | 24 +- .../SourceFiles/history/history_media_types.h | 3 +- .../SourceFiles/media/media_clip_reader.cpp | 18 +- Telegram/gyp/telegram_sources.txt | 2 + 10 files changed, 375 insertions(+), 11 deletions(-) create mode 100644 Telegram/SourceFiles/data/data_document_good_thumbnail.cpp create mode 100644 Telegram/SourceFiles/data/data_document_good_thumbnail.h diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index 7418cfef3b..d5931c7dbb 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_session.h" +#include "data/data_document_good_thumbnail.h" #include "lang/lang_keys.h" #include "inline_bots/inline_bot_layout_item.h" #include "mainwidget.h" @@ -528,6 +529,31 @@ void DocumentData::setattributes(const QVector &attributes _additional = nullptr; } } + validateGoodThumbnail(); +} + +Storage::Cache::Key DocumentData::goodThumbnailCacheKey() const { + return Data::DocumentThumbCacheKey(_dc, id); +} + +Image *DocumentData::goodThumbnail() const { + return _goodThumbnail.get(); +} + +void DocumentData::validateGoodThumbnail() { + if (!isVideoFile() && !isAnimation()) { + _goodThumbnail = nullptr; + } else if (!_goodThumbnail && hasRemoteLocation()) { + _goodThumbnail = std::make_unique( + std::make_unique(this)); + } +} + +void DocumentData::refreshGoodThumbnail() { + if (_goodThumbnail && !_goodThumbnail->loaded()) { + _goodThumbnail->replaceSource( + std::make_unique(this)); + } } bool DocumentData::saveToCache() const { @@ -706,6 +732,8 @@ bool DocumentData::loaded(FilePathResolveType type) const { || (that->sticker() && !that->sticker()->img->isNull())) { ActiveCache().up(that); } + + that->refreshGoodThumbnail(); destroyLoaderDelayed(); } _session->data().notifyDocumentLayoutChanged(this); @@ -1281,6 +1309,7 @@ void DocumentData::setRemoteLocation( } } } + validateGoodThumbnail(); } void DocumentData::setContentUrl(const QString &url) { diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index bc508518a0..ad33709912 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -154,6 +154,11 @@ public: bool hasGoodStickerThumb() const; + Image *goodThumbnail() const; + Storage::Cache::Key goodThumbnailCacheKey() const; + void validateGoodThumbnail(); + void refreshGoodThumbnail(); + void setRemoteLocation( int32 dc, uint64 access, @@ -218,6 +223,8 @@ private: QString _mimeString; WebFileLocation _urlLocation; + std::unique_ptr _goodThumbnail; + not_null _session; FileLocation _location; diff --git a/Telegram/SourceFiles/data/data_document_good_thumbnail.cpp b/Telegram/SourceFiles/data/data_document_good_thumbnail.cpp new file mode 100644 index 0000000000..5ad2220bfa --- /dev/null +++ b/Telegram/SourceFiles/data/data_document_good_thumbnail.cpp @@ -0,0 +1,217 @@ +/* +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 "data/data_document_good_thumbnail.h" + +#include "data/data_session.h" +#include "data/data_document.h" +#include "media/media_clip_reader.h" +#include "auth_session.h" + +namespace Data { +namespace { + +constexpr auto kGoodThumbQuality = 87; + +} // namespace + +GoodThumbSource::GoodThumbSource(not_null document) +: _document(document) { +} + +void GoodThumbSource::generate(base::binary_guard &&guard) { + if (!guard.alive()) { + return; + } + const auto data = _document->data(); + auto location = _document->location().isEmpty() + ? nullptr + : std::make_unique(_document->location()); + if (data.isEmpty() && !location) { + _empty = true; + return; + } + crl::async([ + =, + guard = std::move(guard), + location = std::move(location) + ]() mutable { + const auto filepath = (location && location->accessEnable()) + ? location->name() + : QString(); + auto result = Media::Clip::PrepareForSending(filepath, data); + auto bytes = QByteArray(); + if (!result.thumbnail.isNull()) { + QBuffer buffer(&bytes); + result.thumbnail.save(&buffer, "JPG", kGoodThumbQuality); + } + if (!filepath.isEmpty()) { + location->accessDisable(); + } + ready( + std::move(guard), + std::move(result.thumbnail), + std::move(bytes)); + }); +} + +// NB: This method is called from crl::async(), 'this' is unreliable. +void GoodThumbSource::ready( + base::binary_guard &&guard, + QImage &&image, + QByteArray &&bytes) { + crl::on_main([ + =, + guard = std::move(guard), + image = std::move(image), + bytes = std::move(bytes) + ]() mutable { + if (!guard.alive()) { + return; + } + if (image.isNull()) { + _empty = true; + return; + } + _loaded = std::move(image); + _width = _loaded.width(); + _height = _loaded.height(); + if (!bytes.isEmpty()) { + Auth().data().cache().put( + _document->goodThumbnailCacheKey(), + Storage::Cache::Database::TaggedValue{ + std::move(bytes), + Data::kImageCacheTag }); + } + Auth().downloaderTaskFinished().notify(); + }); +} + +void GoodThumbSource::load( + Data::FileOrigin origin, + bool loadFirst, + bool prior) { + if (loading() || _empty) { + return; + } + auto [left, right] = base::make_binary_guard(); + _loading = std::move(left); + + auto callback = [=, guard = std::move(right)]( + QByteArray &&value) mutable { + if (value.isEmpty()) { + crl::on_main([=, guard = std::move(guard)]() mutable { + generate(std::move(guard)); + }); + return; + } + crl::async([ + =, + guard = std::move(guard), + value = std::move(value) + ]() mutable { + ready(std::move(guard), App::readImage(value)); + }); + }; + + Auth().data().cache().get( + _document->goodThumbnailCacheKey(), + std::move(callback)); +} + +void GoodThumbSource::loadEvenCancelled( + Data::FileOrigin origin, + bool loadFirst, + bool prior) { + _empty = false; + load(origin, loadFirst, prior); +} + +QImage GoodThumbSource::takeLoaded() { + return std::move(_loaded); +} + +void GoodThumbSource::unload() { + _loaded = QImage(); + cancel(); +} + +void GoodThumbSource::automaticLoad( + Data::FileOrigin origin, + const HistoryItem *item) { +} + +void GoodThumbSource::automaticLoadSettingsChanged() { +} + +bool GoodThumbSource::loading() { + return _loading.alive(); +} + +bool GoodThumbSource::displayLoading() { + return false; +} + +void GoodThumbSource::cancel() { + _loading.kill(); +} + +float64 GoodThumbSource::progress() { + return 1.; +} + +int GoodThumbSource::loadOffset() { + return 0; +} + +const StorageImageLocation &GoodThumbSource::location() { + return StorageImageLocation::Null; +} + +void GoodThumbSource::refreshFileReference(const QByteArray &data) { +} + +std::optional GoodThumbSource::cacheKey() { + return _document->goodThumbnailCacheKey(); +} + +void GoodThumbSource::setDelayedStorageLocation( + const StorageImageLocation &location) { +} + +void GoodThumbSource::performDelayedLoad(Data::FileOrigin origin) { +} + +bool GoodThumbSource::isDelayedStorageImage() const { + return false; +} + +void GoodThumbSource::setImageBytes(const QByteArray &bytes) { + if (!bytes.isEmpty()) { + cancel(); + _loaded = App::readImage(bytes); + } +} + +int GoodThumbSource::width() { + return _width; +} + +int GoodThumbSource::height() { + return _height; +} + +void GoodThumbSource::setInformation(int size, int width, int height) { + _width = width; + _height = height; +} + +QByteArray GoodThumbSource::bytesForCache() { + return QByteArray(); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_document_good_thumbnail.h b/Telegram/SourceFiles/data/data_document_good_thumbnail.h new file mode 100644 index 0000000000..f22c36d0ee --- /dev/null +++ b/Telegram/SourceFiles/data/data_document_good_thumbnail.h @@ -0,0 +1,75 @@ +/* +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 "ui/image/image.h" + +class DocumentData; + +namespace Data { + +class GoodThumbSource : public Images::Source { +public: + explicit GoodThumbSource(not_null document); + + void load( + Data::FileOrigin origin, + bool loadFirst, + bool prior) override; + void loadEvenCancelled( + Data::FileOrigin origin, + bool loadFirst, + bool prior) override; + QImage takeLoaded() override; + void unload() override; + + void automaticLoad( + Data::FileOrigin origin, + const HistoryItem *item) override; + void automaticLoadSettingsChanged() override; + + bool loading() override; + bool displayLoading() override; + void cancel() override; + float64 progress() override; + int loadOffset() override; + + const StorageImageLocation &location() override; + void refreshFileReference(const QByteArray &data) override; + std::optional cacheKey() override; + void setDelayedStorageLocation( + const StorageImageLocation &location) override; + void performDelayedLoad(Data::FileOrigin origin) override; + bool isDelayedStorageImage() const override; + void setImageBytes(const QByteArray &bytes) override; + + int width() override; + int height() override; + void setInformation(int size, int width, int height) override; + + QByteArray bytesForCache() override; + +private: + void generate(base::binary_guard &&guard); + + // NB: This method is called from crl::async(), 'this' is unreliable. + void ready( + base::binary_guard &&guard, + QImage &&image, + QByteArray &&bytes = {}); + + not_null _document; + QImage _loaded; + base::binary_guard _loading; + int _width = 0; + int _height = 0; + bool _empty = false; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_types.cpp b/Telegram/SourceFiles/data/data_types.cpp index 0645053554..1b78bac200 100644 --- a/Telegram/SourceFiles/data/data_types.cpp +++ b/Telegram/SourceFiles/data/data_types.cpp @@ -17,6 +17,8 @@ namespace { constexpr auto kDocumentCacheTag = 0x0000000000000100ULL; constexpr auto kDocumentCacheMask = 0x00000000000000FFULL; +constexpr auto kDocumentThumbCacheTag = 0x0000000000000200ULL; +constexpr auto kDocumentThumbCacheMask = 0x00000000000000FFULL; constexpr auto kStorageCacheTag = 0x0000010000000000ULL; constexpr auto kStorageCacheMask = 0x000000FFFFFFFFFFULL; constexpr auto kWebDocumentCacheTag = 0x0000020000000000ULL; @@ -35,6 +37,14 @@ Storage::Cache::Key DocumentCacheKey(int32 dcId, uint64 id) { }; } +Storage::Cache::Key DocumentThumbCacheKey(int32 dcId, uint64 id) { + const auto part = (uint64(dcId) & Data::kDocumentThumbCacheMask); + return Storage::Cache::Key{ + Data::kDocumentThumbCacheTag | part, + id + }; +} + Storage::Cache::Key StorageCacheKey(const StorageImageLocation &location) { const auto dcId = uint64(location.dc()) & 0xFFULL; return Storage::Cache::Key{ diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 45bb702a8d..01ee9be274 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -38,6 +38,7 @@ struct UploadState { }; Storage::Cache::Key DocumentCacheKey(int32 dcId, uint64 id); +Storage::Cache::Key DocumentThumbCacheKey(int32 dcId, uint64 id); Storage::Cache::Key StorageCacheKey(const StorageImageLocation &location); Storage::Cache::Key WebDocumentCacheKey(const WebFileLocation &location); Storage::Cache::Key UrlCacheKey(const QString &location); diff --git a/Telegram/SourceFiles/history/history_media_types.cpp b/Telegram/SourceFiles/history/history_media_types.cpp index 82f5e6177e..289266453f 100644 --- a/Telegram/SourceFiles/history/history_media_types.cpp +++ b/Telegram/SourceFiles/history/history_media_types.cpp @@ -817,6 +817,8 @@ QSize HistoryVideo::countOptimalSize() { } _thumbw = qMax(tw, 1); + _thumbh = qMax(th, 1); + auto minWidth = qMax(st::minPhotoSize, _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); minWidth = qMax(minWidth, documentMaxStatusWidth(_data) + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); auto maxWidth = qMax(_thumbw, minWidth); @@ -852,6 +854,7 @@ QSize HistoryVideo::countCurrentSize(int newWidth) { } _thumbw = qMax(tw, 1); + _thumbh = qMax(th, 1); auto minWidth = qMax(st::minPhotoSize, _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); minWidth = qMax(minWidth, documentMaxStatusWidth(_data) + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); newWidth = qMax(_thumbw, minWidth); @@ -905,7 +908,16 @@ void HistoryVideo::draw(Painter &p, const QRect &r, TextSelection selection, Tim auto roundCorners = inWebPage ? RectPart::AllCorners : ((isBubbleTop() ? (RectPart::TopLeft | RectPart::TopRight) : RectPart::None) | ((isBubbleBottom() && _caption.isEmpty()) ? (RectPart::BottomLeft | RectPart::BottomRight) : RectPart::None)); QRect rthumb(rtlrect(paintx, painty, paintw, painth, width())); - p.drawPixmap(rthumb.topLeft(), _data->thumb->pixBlurredSingle(_realParent->fullId(), _thumbw, 0, paintw, painth, roundRadius, roundCorners)); + + const auto good = _data->goodThumbnail(); + if (good && good->loaded()) { + p.drawPixmap(rthumb.topLeft(), good->pixSingle({}, _thumbw, _thumbh, paintw, painth, roundRadius, roundCorners)); + } else { + if (good) { + good->load({}); + } + p.drawPixmap(rthumb.topLeft(), _data->thumb->pixBlurredSingle(_realParent->fullId(), _thumbw, _thumbh, paintw, painth, roundRadius, roundCorners)); + } if (selected) { App::complexOverlayRect(p, rthumb, roundRadius, roundCorners); } @@ -2257,7 +2269,15 @@ void HistoryGif::draw(Painter &p, const QRect &r, TextSelection selection, TimeM } } } else { - p.drawPixmap(rthumb.topLeft(), _data->thumb->pixBlurredSingle(_realParent->fullId(), _thumbw, _thumbh, usew, painth, roundRadius, roundCorners)); + const auto good = _data->goodThumbnail(); + if (good && good->loaded()) { + p.drawPixmap(rthumb.topLeft(), good->pixSingle({}, _thumbw, _thumbh, usew, painth, roundRadius, roundCorners)); + } else { + if (good) { + good->load({}); + } + p.drawPixmap(rthumb.topLeft(), _data->thumb->pixBlurredSingle(_realParent->fullId(), _thumbw, _thumbh, usew, painth, roundRadius, roundCorners)); + } } if (selected) { diff --git a/Telegram/SourceFiles/history/history_media_types.h b/Telegram/SourceFiles/history/history_media_types.h index 29be982b61..01d20e2e21 100644 --- a/Telegram/SourceFiles/history/history_media_types.h +++ b/Telegram/SourceFiles/history/history_media_types.h @@ -311,7 +311,8 @@ private: void updateStatusText() const; not_null _data; - int _thumbw; + int _thumbw = 1; + int _thumbh = 1; Text _caption; }; diff --git a/Telegram/SourceFiles/media/media_clip_reader.cpp b/Telegram/SourceFiles/media/media_clip_reader.cpp index 38c6a4f23b..80f5791c72 100644 --- a/Telegram/SourceFiles/media/media_clip_reader.cpp +++ b/Telegram/SourceFiles/media/media_clip_reader.cpp @@ -80,14 +80,14 @@ QPixmap PrepareFrame(const FrameRequest &request, const QImage &original, bool h } // namespace -Reader::Reader(const QString &filepath, Callback &&callback, Mode mode, int64 seekMs) +Reader::Reader(const QString &filepath, Callback &&callback, Mode mode, TimeMs seekMs) : _callback(std::move(callback)) , _mode(mode) , _seekPositionMs(seekMs) { init(FileLocation(filepath), QByteArray()); } -Reader::Reader(not_null document, FullMsgId msgId, Callback &&callback, Mode mode, int64 seekMs) +Reader::Reader(not_null document, FullMsgId msgId, Callback &&callback, Mode mode, TimeMs seekMs) : _callback(std::move(callback)) , _mode(mode) , _audioMsgId(document, msgId, (mode == Mode::Video) ? rand_value() : 0) @@ -870,12 +870,14 @@ FileMediaInformation::Video PrepareForSending(const QString &fname, const QByteA auto durationMs = reader->durationMs(); if (durationMs > 0) { result.isGifv = reader->isGifv(); - if (!result.isGifv) { - auto middleMs = durationMs / 2; - if (!reader->inspectAt(middleMs)) { - return result; - } - } + // Use first video frame as a thumbnail. + // All other apps and server do that way. + //if (!result.isGifv) { + // auto middleMs = durationMs / 2; + // if (!reader->inspectAt(middleMs)) { + // return result; + // } + //} auto hasAlpha = false; auto readResult = reader->readFramesTill(-1, getms()); auto readFrame = (readResult == internal::ReaderImplementation::ReadResult::Success); diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt index 323be2a328..a1dd3ef011 100644 --- a/Telegram/gyp/telegram_sources.txt +++ b/Telegram/gyp/telegram_sources.txt @@ -136,6 +136,8 @@ <(src_loc)/data/data_channel_admins.h <(src_loc)/data/data_document.cpp <(src_loc)/data/data_document.h +<(src_loc)/data/data_document_good_thumbnail.cpp +<(src_loc)/data/data_document_good_thumbnail.h <(src_loc)/data/data_drafts.cpp <(src_loc)/data/data_drafts.h <(src_loc)/data/data_feed.cpp