Generate high quality video thumbnail when loaded.

This commit is contained in:
John Preston 2018-10-25 12:12:56 +04:00
parent 8f387891e2
commit da358615e0
10 changed files with 375 additions and 11 deletions

View File

@ -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<MTPDocumentAttribute> &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<Image>(
std::make_unique<Data::GoodThumbSource>(this));
}
}
void DocumentData::refreshGoodThumbnail() {
if (_goodThumbnail && !_goodThumbnail->loaded()) {
_goodThumbnail->replaceSource(
std::make_unique<Data::GoodThumbSource>(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) {

View File

@ -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<Image> _goodThumbnail;
not_null<AuthSession*> _session;
FileLocation _location;

View File

@ -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<DocumentData*> 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<FileLocation>(_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<Storage::Cache::Key> 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

View File

@ -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<DocumentData*> 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<Storage::Cache::Key> 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<DocumentData*> _document;
QImage _loaded;
base::binary_guard _loading;
int _width = 0;
int _height = 0;
bool _empty = false;
};
} // namespace Data

View File

@ -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{

View File

@ -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);

View File

@ -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) {

View File

@ -311,7 +311,8 @@ private:
void updateStatusText() const;
not_null<DocumentData*> _data;
int _thumbw;
int _thumbw = 1;
int _thumbh = 1;
Text _caption;
};

View File

@ -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<DocumentData*> document, FullMsgId msgId, Callback &&callback, Mode mode, int64 seekMs)
Reader::Reader(not_null<DocumentData*> document, FullMsgId msgId, Callback &&callback, Mode mode, TimeMs seekMs)
: _callback(std::move(callback))
, _mode(mode)
, _audioMsgId(document, msgId, (mode == Mode::Video) ? rand_value<uint32>() : 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);

View File

@ -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