Render webm stickers in StickersListWidget.

This commit is contained in:
John Preston 2022-01-25 00:40:26 +03:00
parent 20dbf18106
commit 8e749173de
12 changed files with 483 additions and 319 deletions

View file

@ -46,6 +46,8 @@ namespace {
constexpr auto kSearchRequestDelay = 400;
constexpr auto kInlineItemsMaxPerRow = 5;
constexpr auto kSearchBotUsername = "gif"_cs;
constexpr auto kMinRepaintDelay = crl::time(16);
constexpr auto kMinAfterScrollDelay = crl::time(33);
} // namespace
@ -173,7 +175,9 @@ GifsListWidget::GifsListWidget(
, _mosaic(st::emojiPanWidth - st::inlineResultsLeft)
, _previewTimer([=] { showPreview(); }) {
setMouseTracking(true);
setAttribute(Qt::WA_OpaquePaintEvent);
// Otherwise our optimization on repainting is too aggressive.
setAttribute(Qt::WA_OpaquePaintEvent, false);
_inlineRequestTimer.setSingleShot(true);
connect(
@ -239,7 +243,7 @@ void GifsListWidget::visibleTopBottomUpdated(
auto top = getVisibleTop();
Inner::visibleTopBottomUpdated(visibleTop, visibleBottom);
if (top != getVisibleTop()) {
_lastScrolled = crl::now();
_lastScrolledAt = crl::now();
}
checkLoadMore();
}
@ -498,7 +502,7 @@ void GifsListWidget::clearSelection() {
setCursor(style::cur_default);
}
_selected = _pressed = -1;
update();
repaintItems();
}
TabbedSelector::InnerFooter *GifsListWidget::getFooter() const {
@ -544,7 +548,7 @@ void GifsListWidget::refreshSavedGifs() {
deleteUnusedGifLayouts();
resizeToWidth(width());
update();
repaintItems();
}
if (isVisible()) {
@ -672,7 +676,7 @@ int GifsListWidget::refreshInlineRows(const InlineCacheEntry *entry, bool result
}
resizeToWidth(width());
update();
repaintItems();
_lastMousePos = QCursor::pos();
updateSelected();
@ -711,16 +715,13 @@ void GifsListWidget::inlineItemLayoutChanged(const InlineBots::Layout::ItemBase
}
}
void GifsListWidget::inlineItemRepaint(const InlineBots::Layout::ItemBase *layout) {
auto ms = crl::now();
if (_lastScrolled + 100 <= ms) {
update();
} else {
_updateInlineItems.callOnce(_lastScrolled + 100 - ms);
}
void GifsListWidget::inlineItemRepaint(
const InlineBots::Layout::ItemBase *layout) {
updateInlineItems();
}
bool GifsListWidget::inlineItemVisible(const InlineBots::Layout::ItemBase *layout) {
bool GifsListWidget::inlineItemVisible(
const InlineBots::Layout::ItemBase *layout) {
auto position = layout->position();
if (position < 0 || !isVisible()) {
return false;
@ -930,12 +931,22 @@ void GifsListWidget::showPreview() {
}
void GifsListWidget::updateInlineItems() {
auto ms = crl::now();
if (_lastScrolled + 100 <= ms) {
update();
} else {
_updateInlineItems.callOnce(_lastScrolled + 100 - ms);
const auto now = crl::now();
const auto delay = std::max(
_lastScrolledAt + kMinAfterScrollDelay - now,
_lastUpdatedAt + kMinRepaintDelay - now);
if (delay <= 0) {
repaintItems();
} else if (!_updateInlineItems.isActive()
|| _updateInlineItems.remainingTime() > kMinRepaintDelay) {
_updateInlineItems.callOnce(std::max(delay, kMinRepaintDelay));
}
}
void GifsListWidget::repaintItems(crl::time now) {
_lastUpdatedAt = now ? now : crl::now();
update();
}
} // namespace ChatHelpers

View file

@ -133,12 +133,14 @@ private:
void paintInlineItems(Painter &p, QRect clip);
void updateInlineItems();
void repaintItems(crl::time now = 0);
void showPreview();
MTP::Sender _api;
Section _section = Section::Gifs;
crl::time _lastScrolled = 0;
crl::time _lastScrolledAt = 0;
crl::time _lastUpdatedAt = 0;
base::Timer _updateInlineItems;
bool _inlineWithThumb = false;

View file

@ -111,7 +111,7 @@ struct StickerIcon {
};
class StickersListWidget::Footer : public TabbedSelector::InnerFooter {
class StickersListWidget::Footer final : public TabbedSelector::InnerFooter {
public:
explicit Footer(
not_null<StickersListWidget*> parent,
@ -209,6 +209,16 @@ private:
};
struct StickersListWidget::Sticker {
not_null<DocumentData*> document;
std::shared_ptr<Data::DocumentMedia> documentMedia;
Lottie::Animation *lottie = nullptr;
Media::Clip::ReaderPointer webm;
QPixmap savedFrame;
void ensureMediaCreated();
};
auto StickersListWidget::PrepareStickers(
const QVector<DocumentData*> &pack)
-> std::vector<Sticker> {
@ -284,6 +294,7 @@ void StickersListWidget::Footer::clearHeavyData() {
count);
for (auto i = 0; i != count; ++i) {
auto &icon = _icons[i];
icon.webm = nullptr;
icon.lottie = nullptr;
icon.lifetime.destroy();
icon.stickerMedia = nullptr;
@ -737,6 +748,7 @@ void StickersListWidget::Footer::refreshIcons(
if (const auto i = indices.find(now.setId); i != end(indices)) {
auto &was = _icons[i->second];
if (now.sticker == was.sticker) {
now.webm = std::move(was.webm);
now.lottie = std::move(was.lottie);
now.lifetime = std::move(was.lifetime);
now.savedFrame = std::move(was.savedFrame);
@ -979,7 +991,7 @@ StickersListWidget::StickersListWidget(
session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
if (isVisible()) {
update();
repaintItems();
readVisibleFeatured(getVisibleTop(), getVisibleBottom());
}
}, lifetime());
@ -1135,7 +1147,7 @@ void StickersListWidget::preloadMoreOfficial() {
}
});
resizeToWidth(width());
update();
repaintItems();
}).send();
}
@ -1499,8 +1511,8 @@ void StickersListWidget::takeHeavyData(Set &to, Set &from) {
}
}
for (const auto &sticker : fromList) {
if (sticker.animated) {
to.lottiePlayer->remove(sticker.animated);
if (sticker.lottie) {
to.lottiePlayer->remove(sticker.lottie);
}
}
}
@ -1509,7 +1521,8 @@ void StickersListWidget::takeHeavyData(Set &to, Set &from) {
void StickersListWidget::takeHeavyData(Sticker &to, Sticker &from) {
to.documentMedia = std::move(from.documentMedia);
to.savedFrame = std::move(from.savedFrame);
to.animated = base::take(from.animated);
to.lottie = base::take(from.lottie);
to.webm = base::take(from.webm);
}
auto StickersListWidget::shownSets() const -> const std::vector<Set> & {
@ -1629,6 +1642,9 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
? &_pressed
: &_selected);
const auto now = crl::now();
const auto paused = controller()->isGifPausedAtLeastFor(
Window::GifPauseReason::SavedGifs);
if (sets.empty() && _section == Section::Search) {
paintEmptySearchResults(p);
}
@ -1708,9 +1724,11 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
auto selected = selectedSticker ? (selectedSticker->section == info.section && selectedSticker->index == index) : false;
auto deleteSelected = false;
paintSticker(p, set, info.rowsTop, info.section, index, selected, deleteSelected);
paintSticker(p, set, info.rowsTop, info.section, index, now, paused, selected, deleteSelected);
}
if (!paused) {
markLottieFrameShown(set);
}
markLottieFrameShown(set);
return true;
}
if (setHasTitle(set) && clip.top() < info.rowsTop) {
@ -1754,21 +1772,19 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
auto selected = selectedSticker ? (selectedSticker->section == info.section && selectedSticker->index == index) : false;
auto deleteSelected = selected && selectedSticker->overDelete;
paintSticker(p, set, info.rowsTop, info.section, index, selected, deleteSelected);
paintSticker(p, set, info.rowsTop, info.section, index, now, paused, selected, deleteSelected);
}
}
markLottieFrameShown(set);
if (!paused) {
markLottieFrameShown(set);
}
return true;
});
}
void StickersListWidget::markLottieFrameShown(Set &set) {
if (const auto player = set.lottiePlayer.get()) {
const auto paused = controller()->isGifPausedAtLeastFor(
Window::GifPauseReason::SavedGifs);
if (!paused) {
player->markFrameShown();
}
player->markFrameShown();
}
}
@ -1801,7 +1817,8 @@ void StickersListWidget::clearHeavyIn(Set &set, bool clearSavedFrames) {
if (clearSavedFrames) {
sticker.savedFrame = QPixmap();
}
sticker.animated = nullptr;
sticker.webm = nullptr;
sticker.lottie = nullptr;
sticker.documentMedia = nullptr;
}
}
@ -1821,7 +1838,7 @@ void StickersListWidget::pauseInvisibleLottieIn(const SectionInfo &info) {
if (index >= info.count) {
break;
}
if (const auto animated = set.stickers[index].animated) {
if (const auto animated = set.stickers[index].lottie) {
player->pause(animated);
}
}
@ -1903,16 +1920,13 @@ void StickersListWidget::ensureLottiePlayer(Set &set) {
raw->updates(
) | rpl::start_with_next([=] {
auto &sets = shownSets();
enumerateSections([&](const SectionInfo &info) {
if (shownSets()[info.section].lottiePlayer.get() == raw) {
update(
0,
info.rowsTop,
width(),
info.rowsBottom - info.rowsTop);
return false;
if (sets[info.section].lottiePlayer.get() != raw) {
return true;
}
return true;
repaintItems(info);
return false;
});
}, set.lottieLifetime);
}
@ -1923,20 +1937,113 @@ void StickersListWidget::setupLottie(Set &set, int section, int index) {
// Document should be loaded already for the animation to be set up.
Assert(sticker.documentMedia != nullptr);
sticker.animated = LottieAnimationFromDocument(
sticker.lottie = LottieAnimationFromDocument(
set.lottiePlayer.get(),
sticker.documentMedia.get(),
StickerLottieSize::StickersPanel,
boundingBoxSize() * cIntRetinaFactor());
}
void StickersListWidget::setupWebm(Set &set, int section, int index) {
auto &sticker = set.stickers[index];
// Document should be loaded already for the animation to be set up.
Assert(sticker.documentMedia != nullptr);
const auto setId = set.id;
const auto document = sticker.document;
auto callback = [=](Media::Clip::Notification notification) {
clipCallback(notification, setId, document, index);
};
sticker.webm = Media::Clip::MakeReader(
sticker.documentMedia->owner()->location(),
sticker.documentMedia->bytes(),
std::move(callback));
}
void StickersListWidget::clipCallback(
Media::Clip::Notification notification,
uint64 setId,
not_null<DocumentData*> document,
int indexHint) {
Expects(indexHint >= 0);
auto &sets = shownSets();
enumerateSections([&](const SectionInfo &info) {
auto &set = sets[info.section];
if (set.id != setId) {
return true;
}
using namespace Media::Clip;
switch (notification) {
case Notification::Reinit: {
const auto j = (indexHint < set.stickers.size()
&& set.stickers[indexHint].document == document)
? (begin(set.stickers) + indexHint)
: ranges::find(set.stickers, document, &Sticker::document);
if (j == end(set.stickers) || !j->webm) {
break;
}
const auto index = j - begin(set.stickers);
auto &webm = j->webm;
if (webm->state() == State::Error) {
webm.setBad();
} else if (webm->ready() && !webm->started()) {
const auto size = ComputeStickerSize(
j->document,
boundingBoxSize());
webm->start({ .frame = size, .keepAlpha = true });
} else if (webm->autoPausedGif() && !itemVisible(info, index)) {
webm = nullptr;
}
} break;
case Notification::Repaint: break;
}
repaintItems(info);
return false;
});
}
bool StickersListWidget::itemVisible(
const SectionInfo &info,
int index) const {
const auto visibleTop = getVisibleTop();
const auto visibleBottom = getVisibleBottom();
const auto row = index / _columnCount;
const auto top = info.rowsTop + row * _singleSize.height();
const auto bottom = top + _singleSize.height();
return (visibleTop < bottom) && (visibleBottom > top);
}
void StickersListWidget::repaintItems(const SectionInfo &info) {
update(
0,
info.rowsTop,
width(),
info.rowsBottom - info.rowsTop);
}
void StickersListWidget::repaintItems() {
}
QSize StickersListWidget::boundingBoxSize() const {
return QSize(
_singleSize.width() - st::roundRadiusSmall * 2,
_singleSize.height() - st::roundRadiusSmall * 2);
}
void StickersListWidget::paintSticker(Painter &p, Set &set, int y, int section, int index, bool selected, bool deleteSelected) {
void StickersListWidget::paintSticker(
Painter &p,
Set &set,
int y,
int section,
int index,
crl::time now,
bool paused,
bool selected,
bool deleteSelected) {
auto &sticker = set.stickers[index];
sticker.ensureMediaCreated();
const auto document = sticker.document;
@ -1946,10 +2053,13 @@ void StickersListWidget::paintSticker(Painter &p, Set &set, int y, int section,
}
const auto isLottie = document->sticker()->isLottie();
const auto isWebm = document->sticker()->isWebm();
if (isLottie
&& !sticker.animated
&& !sticker.lottie
&& media->loaded()) {
setupLottie(set, section, index);
} else if (isWebm && !sticker.webm && media->loaded()) {
setupWebm(set, section, index);
}
int row = (index / _columnCount), col = (index % _columnCount);
@ -1963,25 +2073,15 @@ void StickersListWidget::paintSticker(Painter &p, Set &set, int y, int section,
media->checkStickerSmall();
auto w = 1;
auto h = 1;
if (isLottie && !document->dimensions.isEmpty()) {
const auto request = Lottie::FrameRequest{ boundingBoxSize() * cIntRetinaFactor() };
const auto size = request.size(document->dimensions, true) / cIntRetinaFactor();
w = std::max(size.width(), 1);
h = std::max(size.height(), 1);
} else {
auto coef = qMin((_singleSize.width() - st::roundRadiusSmall * 2) / float64(document->dimensions.width()), (_singleSize.height() - st::roundRadiusSmall * 2) / float64(document->dimensions.height()));
if (coef > 1) coef = 1;
w = std::max(qRound(coef * document->dimensions.width()), 1);
h = std::max(qRound(coef * document->dimensions.height()), 1);
}
auto ppos = pos + QPoint((_singleSize.width() - w) / 2, (_singleSize.height() - h) / 2);
const auto size = ComputeStickerSize(document, boundingBoxSize());
const auto ppos = pos + QPoint(
(_singleSize.width() - size.width()) / 2,
(_singleSize.height() - size.height()) / 2);
if (sticker.animated && sticker.animated->ready()) {
if (sticker.lottie && sticker.lottie->ready()) {
auto request = Lottie::FrameRequest();
request.box = boundingBoxSize() * cIntRetinaFactor();
const auto frame = sticker.animated->frame(request);
const auto frame = sticker.lottie->frame(request);
p.drawImage(
QRect(ppos, frame.size() / cIntRetinaFactor()),
frame);
@ -1989,13 +2089,20 @@ void StickersListWidget::paintSticker(Painter &p, Set &set, int y, int section,
sticker.savedFrame = QPixmap::fromImage(frame, Qt::ColorOnly);
sticker.savedFrame.setDevicePixelRatio(cRetinaFactor());
}
set.lottiePlayer->unpause(sticker.animated);
set.lottiePlayer->unpause(sticker.lottie);
} else if (sticker.webm && sticker.webm->ready()) {
p.drawPixmapLeft(
ppos,
width(),
sticker.webm->current(
{ .frame = size, .keepAlpha = true },
now));
} else {
const auto image = media->getStickerSmall();
const auto pixmap = !sticker.savedFrame.isNull()
? sticker.savedFrame
: image
? image->pixSingle(w, h, { .outer = { w, h } })
? image->pixSingle(size, { .outer = size })
: QPixmap();
if (!pixmap.isNull()) {
p.drawPixmapLeft(ppos, width(), pixmap);
@ -2006,7 +2113,7 @@ void StickersListWidget::paintSticker(Painter &p, Set &set, int y, int section,
PaintStickerThumbnailPath(
p,
media.get(),
QRect(ppos, QSize{ w, h }),
QRect(ppos, size),
_pathGradient.get());
}
}
@ -2227,7 +2334,7 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) {
auto pressed = _pressed;
setPressed(v::null);
if (pressed != _selected) {
update();
repaintItems();
}
auto activated = ClickHandler::unpressed();
@ -2329,7 +2436,7 @@ void StickersListWidget::removeRecentSticker(int section, int index) {
if (refresh) {
refreshRecentStickers();
updateSelected();
update();
repaintItems();
}
}
@ -2389,7 +2496,7 @@ void StickersListWidget::enterFromChildEvent(QEvent *e, QWidget *child) {
void StickersListWidget::clearSelection() {
setPressed(v::null);
setSelected(v::null);
update();
repaintItems();
}
TabbedSelector::InnerFooter *StickersListWidget::getFooter() const {
@ -2449,7 +2556,7 @@ void StickersListWidget::refreshStickers() {
_lastMousePosition = QCursor::pos();
updateSelected();
update();
repaintItems();
}
void StickersListWidget::refreshMySets() {
@ -3031,7 +3138,7 @@ void StickersListWidget::showStickerSet(uint64 setId) {
if (_footer) {
_footer->refreshIcons(ValidateIconAnimations::Scroll);
}
update();
repaintItems();
}
scrollTo(0);
@ -3063,7 +3170,7 @@ void StickersListWidget::showStickerSet(uint64 setId) {
_lastMousePosition = QCursor::pos();
update();
repaintItems();
}
void StickersListWidget::refreshMegagroupSetGeometry() {

View file

@ -39,6 +39,11 @@ class DocumentMedia;
class StickersSet;
} // namespace Data
namespace Media::Clip {
class ReaderPointer;
enum class Notification;
} // namespace Media::Clip
namespace ChatHelpers {
struct StickerIcon;
@ -113,6 +118,7 @@ protected:
private:
class Footer;
struct Sticker;
enum class Section {
Featured,
@ -178,15 +184,6 @@ private:
int rowsBottom = 0;
};
struct Sticker {
not_null<DocumentData*> document;
std::shared_ptr<Data::DocumentMedia> documentMedia;
Lottie::Animation *animated = nullptr;
QPixmap savedFrame;
void ensureMediaCreated();
};
struct Set {
Set(
uint64 id,
@ -279,11 +276,27 @@ private:
void paintStickers(Painter &p, QRect clip);
void paintMegagroupEmptySet(Painter &p, int y, bool buttonSelected);
void paintSticker(Painter &p, Set &set, int y, int section, int index, bool selected, bool deleteSelected);
void paintSticker(
Painter &p,
Set &set,
int y,
int section,
int index,
crl::time now,
bool paused,
bool selected,
bool deleteSelected);
void paintEmptySearchResults(Painter &p);
void ensureLottiePlayer(Set &set);
void setupLottie(Set &set, int section, int index);
void setupWebm(Set &set, int section, int index);
void clipCallback(
Media::Clip::Notification notification,
uint64 setId,
not_null<DocumentData*> document,
int indexHint);
[[nodiscard]] bool itemVisible(const SectionInfo &info, int index) const;
void markLottieFrameShown(Set &set);
void checkVisibleLottie();
void pauseInvisibleLottieIn(const SectionInfo &info);
@ -292,6 +305,8 @@ private:
void takeHeavyData(Sticker &to, Sticker &from);
void clearHeavyIn(Set &set, bool clearSavedFrames = true);
void clearHeavyData();
void repaintItems();
void repaintItems(const SectionInfo &info);
int stickersRight() const;
bool featuredHasAddButton(int index) const;
@ -302,8 +317,8 @@ private:
void refreshMegagroupSetGeometry();
QRect megagroupSetButtonRectFinal() const;
const Data::StickersSetsOrder &defaultSetsOrder() const;
Data::StickersSetsOrder &defaultSetsOrderRef();
[[nodiscard]] const Data::StickersSetsOrder &defaultSetsOrder() const;
[[nodiscard]] Data::StickersSetsOrder &defaultSetsOrderRef();
enum class AppendSkip {
None,
@ -316,7 +331,6 @@ private:
bool externalLayout,
AppendSkip skip = AppendSkip::None);
void selectEmoji(EmojiPtr emoji);
int stickersLeft() const;
QRect stickerRect(int section, int sel);

View file

@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_file_origin.h"
#include "storage/cache/storage_cache_database.h"
#include "history/view/media/history_view_media_common.h"
#include "media/clip/media_clip_reader.h"
#include "ui/effects/path_shift_gradient.h"
#include "main/main_session.h"
@ -276,4 +277,15 @@ bool PaintStickerThumbnailPath(
});
}
QSize ComputeStickerSize(not_null<DocumentData*> document, QSize box) {
const auto sticker = document->sticker();
const auto dimensions = document->dimensions;
if (!sticker || !sticker->isLottie() || dimensions.isEmpty()) {
return HistoryView::DownscaledSize(dimensions, box);
}
const auto ratio = style::DevicePixelRatio();
const auto request = Lottie::FrameRequest{ box * ratio };
return HistoryView::NonEmptySize(request.size(dimensions, true) / ratio);
}
} // namespace ChatHelpers

View file

@ -113,4 +113,8 @@ bool PaintStickerThumbnailPath(
QRect target,
not_null<Ui::PathShiftGradient*> gradient);
[[nodiscard]] QSize ComputeStickerSize(
not_null<DocumentData*> document,
QSize box);
} // namespace ChatHelpers

View file

@ -168,19 +168,20 @@ void Gif::paint(Painter &p, const QRect &clip, const PaintContext *context) cons
}
const auto radial = isRadialAnimation();
int32 height = st::inlineMediaHeight;
QSize frame = countFrameSize();
QRect r(0, 0, _width, height);
const auto frame = countFrameSize();
const auto r = QRect(0, 0, _width, st::inlineMediaHeight);
if (animating) {
const auto pixmap = _gif->current(frame.width(), frame.height(), _width, height, ImageRoundRadius::None, RectPart::None, context->paused ? 0 : context->ms);
const auto pixmap = _gif->current({
.frame = frame,
.outer = r.size(),
}, context->paused ? 0 : context->ms);
if (_thumb.isNull()) {
_thumb = pixmap;
_thumbGood = true;
}
p.drawPixmap(r.topLeft(), pixmap);
} else {
prepareThumbnail({ _width, height }, frame);
prepareThumbnail(r.size(), frame);
if (_thumb.isNull()) {
p.fillRect(r, st::overviewPhotoBg);
} else {
@ -211,7 +212,11 @@ void Gif::paint(Painter &p, const QRect &clip, const PaintContext *context) cons
return &st::historyFileInDownload;
}();
const auto size = st::inlineRadialSize;
QRect inner((_width - size) / 2, (height - size) / 2, size, size);
QRect inner(
(r.width() - size) / 2,
(r.height() - size) / 2,
size,
size);
icon->paintInCenter(p, inner);
if (radial) {
p.setOpacity(1);
@ -404,9 +409,10 @@ void Gif::clipCallback(Media::Clip::Notification notification) {
_gif->height());
_gif.reset();
} else {
auto height = st::inlineMediaHeight;
auto frame = countFrameSize();
_gif->start(frame.width(), frame.height(), _width, height, ImageRoundRadius::None, RectPart::None);
_gif->start({
.frame = countFrameSize(),
.outer = { _width, st::inlineMediaHeight },
});
}
} else if (_gif->autoPausedGif() && !context()->inlineItemVisible(this)) {
unloadHeavyPart();
@ -1424,7 +1430,10 @@ void Game::paint(Painter &p, const QRect &clip, const PaintContext *context) con
radial = isRadialAnimation();
if (animating) {
const auto pixmap = _gif->current(_frameSize.width(), _frameSize.height(), st::inlineThumbSize, st::inlineThumbSize, ImageRoundRadius::None, RectPart::None, context->paused ? 0 : context->ms);
const auto pixmap = _gif->current({
.frame = _frameSize,
.outer = { st::inlineThumbSize, st::inlineThumbSize },
}, context->paused ? 0 : context->ms);
if (_thumb.isNull()) {
_thumb = pixmap;
_thumbGood = true;
@ -1594,13 +1603,10 @@ void Game::clipCallback(Media::Clip::Notification notification) {
_gif->height());
_gif.reset();
} else {
_gif->start(
_frameSize.width(),
_frameSize.height(),
st::inlineThumbSize,
st::inlineThumbSize,
ImageRoundRadius::None,
RectPart::None);
_gif->start({
.frame = _frameSize,
.outer = { st::inlineThumbSize, st::inlineThumbSize },
});
}
} else if (_gif->autoPausedGif() && !context()->inlineItemVisible(this)) {
unloadHeavyPart();

View file

@ -35,44 +35,49 @@ constexpr auto kClipThreadsCount = 8;
constexpr auto kAverageGifSize = 320 * 240;
constexpr auto kWaitBeforeGifPause = crl::time(200);
QVector<QThread*> threads;
QVector<Manager*> managers;
QImage PrepareFrameImage(const FrameRequest &request, const QImage &original, bool hasAlpha, QImage &cache) {
auto needResize = (original.width() != request.framew) || (original.height() != request.frameh);
auto needOuterFill = (request.outerw != request.framew) || (request.outerh != request.frameh);
auto needRounding = (request.radius != ImageRoundRadius::None);
const auto needResize = (original.size() != request.frame);
const auto needOuterFill = request.outer.isValid() && (request.outer != request.frame);
const auto needRounding = (request.radius != ImageRoundRadius::None);
if (!needResize && !needOuterFill && !hasAlpha && !needRounding) {
return original;
}
auto factor = request.factor;
auto needNewCache = (cache.width() != request.outerw || cache.height() != request.outerh);
const auto factor = request.factor;
const auto size = request.outer.isValid() ? request.outer : request.frame;
const auto needNewCache = (cache.size() != size);
if (needNewCache) {
cache = QImage(request.outerw, request.outerh, QImage::Format_ARGB32_Premultiplied);
cache = QImage(size, QImage::Format_ARGB32_Premultiplied);
cache.setDevicePixelRatio(factor);
}
if (hasAlpha && request.keepAlpha) {
cache.fill(Qt::transparent);
}
{
Painter p(&cache);
if (needNewCache) {
if (request.framew < request.outerw) {
p.fillRect(0, 0, (request.outerw - request.framew) / (2 * factor), cache.height() / factor, st::imageBg);
p.fillRect((request.outerw - request.framew) / (2 * factor) + (request.framew / factor), 0, (cache.width() / factor) - ((request.outerw - request.framew) / (2 * factor) + (request.framew / factor)), cache.height() / factor, st::imageBg);
const auto framew = request.frame.width();
const auto outerw = size.width();
const auto frameh = request.frame.height();
const auto outerh = size.height();
if (needNewCache && (!hasAlpha || !request.keepAlpha)) {
if (framew < outerw) {
p.fillRect(0, 0, (outerw - framew) / (2 * factor), cache.height() / factor, st::imageBg);
p.fillRect((outerw - framew) / (2 * factor) + (framew / factor), 0, (cache.width() / factor) - ((outerw - framew) / (2 * factor) + (framew / factor)), cache.height() / factor, st::imageBg);
}
if (request.frameh < request.outerh) {
p.fillRect(qMax(0, (request.outerw - request.framew) / (2 * factor)), 0, qMin(cache.width(), request.framew) / factor, (request.outerh - request.frameh) / (2 * factor), st::imageBg);
p.fillRect(qMax(0, (request.outerw - request.framew) / (2 * factor)), (request.outerh - request.frameh) / (2 * factor) + (request.frameh / factor), qMin(cache.width(), request.framew) / factor, (cache.height() / factor) - ((request.outerh - request.frameh) / (2 * factor) + (request.frameh / factor)), st::imageBg);
if (frameh < outerh) {
p.fillRect(qMax(0, (outerw - framew) / (2 * factor)), 0, qMin(cache.width(), framew) / factor, (outerh - frameh) / (2 * factor), st::imageBg);
p.fillRect(qMax(0, (outerw - framew) / (2 * factor)), (outerh - frameh) / (2 * factor) + (frameh / factor), qMin(cache.width(), framew) / factor, (cache.height() / factor) - ((outerh - frameh) / (2 * factor) + (frameh / factor)), st::imageBg);
}
}
if (hasAlpha) {
p.fillRect(qMax(0, (request.outerw - request.framew) / (2 * factor)), qMax(0, (request.outerh - request.frameh) / (2 * factor)), qMin(cache.width(), request.framew) / factor, qMin(cache.height(), request.frameh) / factor, st::imageBgTransparent);
if (hasAlpha && !request.keepAlpha) {
p.fillRect(qMax(0, (outerw - framew) / (2 * factor)), qMax(0, (outerh - frameh) / (2 * factor)), qMin(cache.width(), framew) / factor, qMin(cache.height(), frameh) / factor, st::imageBgTransparent);
}
auto position = QPoint((request.outerw - request.framew) / (2 * factor), (request.outerh - request.frameh) / (2 * factor));
const auto position = QPoint((outerw - framew) / (2 * factor), (outerh - frameh) / (2 * factor));
if (needResize) {
PainterHighQualityEnabler hq(p);
auto dst = QRect(position, QSize(request.framew / factor, request.frameh / factor));
auto src = QRect(0, 0, original.width(), original.height());
const auto dst = QRect(position, QSize(framew / factor, frameh / factor));
const auto src = QRect(0, 0, original.width(), original.height());
p.drawImage(dst, original, src, Qt::ColorOnly);
} else {
p.drawImage(position, original);
@ -93,6 +98,81 @@ QPixmap PrepareFrame(const FrameRequest &request, const QImage &original, bool h
} // namespace
enum class ProcessResult {
Error,
Started,
Finished,
Paused,
Repaint,
CopyFrame,
Wait,
};
class Manager final : public QObject {
public:
explicit Manager(not_null<QThread*> thread);
~Manager();
int loadLevel() const {
return _loadLevel;
}
void append(Reader *reader, const Core::FileLocation &location, const QByteArray &data);
void start(Reader *reader);
void update(Reader *reader);
void stop(Reader *reader);
bool carries(Reader *reader) const;
private:
void process();
void finish();
void callback(Reader *reader, Notification notification);
void clear();
QAtomicInt _loadLevel;
using ReaderPointers = QMap<Reader*, QAtomicInt>;
ReaderPointers _readerPointers;
mutable QMutex _readerPointersMutex;
ReaderPointers::const_iterator constUnsafeFindReaderPointer(ReaderPrivate *reader) const;
ReaderPointers::iterator unsafeFindReaderPointer(ReaderPrivate *reader);
bool handleProcessResult(ReaderPrivate *reader, ProcessResult result, crl::time ms);
enum ResultHandleState {
ResultHandleRemove,
ResultHandleStop,
ResultHandleContinue,
};
ResultHandleState handleResult(ReaderPrivate *reader, ProcessResult result, crl::time ms);
using Readers = QMap<ReaderPrivate*, crl::time>;
Readers _readers;
QTimer _timer;
QThread *_processingInThread = nullptr;
bool _needReProcess = false;
};
namespace {
struct Worker {
Worker() : manager(&thread) {
thread.start();
}
~Worker() {
thread.quit();
thread.wait();
}
QThread thread;
Manager manager;
};
std::vector<std::unique_ptr<Worker>> Workers;
} // namespace
Reader::Reader(
const Core::FileLocation &location,
const QByteArray &data,
@ -112,23 +192,21 @@ Reader::Reader(const QByteArray &data, Callback &&callback)
}
void Reader::init(const Core::FileLocation &location, const QByteArray &data) {
if (threads.size() < kClipThreadsCount) {
_threadIndex = threads.size();
threads.push_back(new QThread());
managers.push_back(new Manager(threads.back()));
threads.back()->start();
if (Workers.size() < kClipThreadsCount) {
_threadIndex = Workers.size();
Workers.push_back(std::make_unique<Worker>());
} else {
_threadIndex = int32(base::RandomValue<uint32>() % threads.size());
int32 loadLevel = 0x7FFFFFFF;
for (int32 i = 0, l = threads.size(); i < l; ++i) {
int32 level = managers.at(i)->loadLevel();
_threadIndex = base::RandomIndex(Workers.size());
auto loadLevel = 0x7FFFFFFF;
for (int i = 0, l = int(Workers.size()); i < l; ++i) {
const auto level = Workers[i]->manager.loadLevel();
if (level < loadLevel) {
_threadIndex = i;
loadLevel = level;
}
}
}
managers.at(_threadIndex)->append(this, location, data);
Workers[_threadIndex]->manager.append(this, location, data);
}
Reader::Frame *Reader::frameToShow(int32 *index) const { // 0 means not ready
@ -207,65 +285,74 @@ void Reader::SafeCallback(
int threadIndex,
Notification notification) {
// Check if reader is not deleted already
if (managers.size() > threadIndex && managers.at(threadIndex)->carries(reader) && reader->_callback) {
if (Workers.size() > threadIndex
&& Workers[threadIndex]->manager.carries(reader)
&& reader->_callback) {
reader->_callback(Notification(notification));
}
}
void Reader::start(int32 framew, int32 frameh, int32 outerw, int32 outerh, ImageRoundRadius radius, RectParts corners) {
if (managers.size() <= _threadIndex) error();
if (_state == State::Error) return;
if (_step.loadAcquire() == kWaitingForRequestStep) {
int factor = style::DevicePixelRatio();
FrameRequest request;
request.factor = factor;
request.framew = framew * factor;
request.frameh = frameh * factor;
request.outerw = outerw * factor;
request.outerh = outerh * factor;
request.radius = radius;
request.corners = corners;
_frames[0].request = _frames[1].request = _frames[2].request = request;
moveToNextShow();
managers.at(_threadIndex)->start(this);
void Reader::start(FrameRequest request) {
if (Workers.size() <= _threadIndex) {
error();
}
if (_state == State::Error
|| (_step.loadAcquire() != kWaitingForRequestStep)) {
return;
}
const auto factor = style::DevicePixelRatio();
request.factor = factor;
request.frame *= factor;
if (request.outer.isValid()) {
request.outer *= factor;
}
_frames[0].request = _frames[1].request = _frames[2].request = request;
moveToNextShow();
Workers[_threadIndex]->manager.start(this);
}
QPixmap Reader::current(int32 framew, int32 frameh, int32 outerw, int32 outerh, ImageRoundRadius radius, RectParts corners, crl::time ms) {
Expects(outerw > 0);
Expects(outerh > 0);
QPixmap Reader::current(FrameRequest request, crl::time now) {
Expects(!(request.outer.isValid()
? request.outer
: request.frame).isEmpty());
auto frame = frameToShow();
const auto frame = frameToShow();
Assert(frame != nullptr);
auto shouldBePaused = !ms;
const auto shouldBePaused = !now;
if (!shouldBePaused) {
frame->displayed.storeRelease(1);
if (_autoPausedGif.loadAcquire()) {
_autoPausedGif.storeRelease(0);
if (managers.size() <= _threadIndex) error();
if (_state != State::Error) {
managers.at(_threadIndex)->update(this);
if (Workers.size() <= _threadIndex) {
error();
} else if (_state != State::Error) {
Workers[_threadIndex]->manager.update(this);
}
}
} else {
frame->displayed.storeRelease(-1);
}
auto factor = style::DevicePixelRatio();
if (frame->pix.width() == outerw * factor
&& frame->pix.height() == outerh * factor
&& frame->request.radius == radius
&& frame->request.corners == corners) {
const auto factor = style::DevicePixelRatio();
request.factor = factor;
request.frame *= factor;
if (request.outer.isValid()) {
request.outer *= factor;
}
const auto size = request.outer.isValid()
? request.outer
: request.frame;
Assert(frame->request.radius == request.radius
&& frame->request.corners == request.corners
&& frame->request.keepAlpha == request.keepAlpha);
if (frame->pix.size() == size) {
moveToNextShow();
return frame->pix;
}
frame->request.framew = framew * factor;
frame->request.frameh = frameh * factor;
frame->request.outerw = outerw * factor;
frame->request.outerh = outerh * factor;
frame->request.frame = request.frame;
frame->request.outer = request.outer;
QImage cacheForResize;
frame->original.setDevicePixelRatio(factor);
@ -277,18 +364,21 @@ QPixmap Reader::current(int32 framew, int32 frameh, int32 outerw, int32 outerh,
moveToNextShow();
if (managers.size() <= _threadIndex) error();
if (_state != State::Error) {
managers.at(_threadIndex)->update(this);
if (Workers.size() <= _threadIndex) {
error();
} else if (_state != State::Error) {
Workers[_threadIndex]->manager.update(this);
}
return frame->pix;
}
bool Reader::ready() const {
if (_width && _height) return true;
if (_width && _height) {
return true;
}
auto frame = frameToShow();
const auto frame = frameToShow();
if (frame) {
_width = frame->original.width();
_height = frame->original.height();
@ -298,7 +388,7 @@ bool Reader::ready() const {
}
crl::time Reader::getPositionMs() const {
if (auto frame = frameToShow()) {
if (const auto frame = frameToShow()) {
return frame->positionMs;
}
return 0;
@ -309,11 +399,13 @@ crl::time Reader::getDurationMs() const {
}
void Reader::pauseResumeVideo() {
if (managers.size() <= _threadIndex) error();
if (Workers.size() <= _threadIndex) {
error();
}
if (_state == State::Error) return;
_videoPauseRequest.storeRelease(1 - _videoPauseRequest.loadAcquire());
managers.at(_threadIndex)->start(this);
Workers[_threadIndex]->manager.start(this);
}
bool Reader::videoPaused() const {
@ -333,9 +425,11 @@ State Reader::state() const {
}
void Reader::stop() {
if (managers.size() <= _threadIndex) error();
if (Workers.size() <= _threadIndex) {
error();
}
if (_state != State::Error) {
managers.at(_threadIndex)->stop(this);
Workers[_threadIndex]->manager.stop(this);
_width = _height = 0;
}
}
@ -461,7 +555,7 @@ public:
bool renderFrame() {
Expects(_request.valid());
if (!_implementation->renderFrame(frame()->original, frame()->alpha, QSize(_request.framew, _request.frameh))) {
if (!_implementation->renderFrame(frame()->original, frame()->alpha, _request.frame)) {
return false;
}
frame()->original.setDevicePixelRatio(_request.factor);
@ -574,7 +668,7 @@ private:
};
Manager::Manager(QThread *thread) {
Manager::Manager(not_null<QThread*> thread) {
moveToThread(thread);
connect(thread, &QThread::started, this, [=] { process(); });
connect(thread, &QThread::finished, this, [=] { finish(); });
@ -884,17 +978,7 @@ Ui::PreparedFileInformation::Video PrepareForSending(const QString &fname, const
}
void Finish() {
if (!threads.isEmpty()) {
for (int32 i = 0, l = threads.size(); i < l; ++i) {
threads.at(i)->quit();
DEBUG_LOG(("Waiting for clipThread to finish: %1").arg(i));
threads.at(i)->wait();
delete managers.at(i);
delete threads.at(i);
}
threads.clear();
managers.clear();
}
Workers.clear();
}
Reader *const ReaderPointer::BadPointer = reinterpret_cast<Reader*>(1);

View file

@ -30,13 +30,12 @@ struct FrameRequest {
bool valid() const {
return factor > 0;
}
QSize frame;
QSize outer;
int factor = 0;
int framew = 0;
int frameh = 0;
int outerw = 0;
int outerh = 0;
ImageRoundRadius radius = ImageRoundRadius::None;
RectParts corners = RectPart::AllCorners;
bool keepAlpha = false;
};
// Before ReaderPrivate read the first image and got the original frame size.
@ -54,6 +53,7 @@ enum class Notification {
Repaint,
};
class Manager;
class ReaderPrivate;
class Reader {
public:
@ -73,40 +73,40 @@ public:
int threadIndex,
Notification notification);
void start(int framew, int frameh, int outerw, int outerh, ImageRoundRadius radius, RectParts corners);
QPixmap current(int framew, int frameh, int outerw, int outerh, ImageRoundRadius radius, RectParts corners, crl::time ms);
QPixmap frameOriginal() const {
if (auto frame = frameToShow()) {
void start(FrameRequest request);
[[nodiscard]] QPixmap current(FrameRequest request, crl::time now);
[[nodiscard]] QPixmap frameOriginal() const {
if (const auto frame = frameToShow()) {
auto result = QPixmap::fromImage(frame->original);
result.detach();
return result;
}
return QPixmap();
}
bool currentDisplayed() const {
auto frame = frameToShow();
return frame ? (frame->displayed.loadAcquire() != 0) : true;
[[nodiscard]] bool currentDisplayed() const {
const auto frame = frameToShow();
return !frame || (frame->displayed.loadAcquire() != 0);
}
bool autoPausedGif() const {
[[nodiscard]] bool autoPausedGif() const {
return _autoPausedGif.loadAcquire();
}
bool videoPaused() const;
int threadIndex() const {
[[nodiscard]] bool videoPaused() const;
[[nodiscard]] int threadIndex() const {
return _threadIndex;
}
int width() const;
int height() const;
[[nodiscard]] int width() const;
[[nodiscard]] int height() const;
State state() const;
bool started() const {
auto step = _step.loadAcquire();
[[nodiscard]] State state() const;
[[nodiscard]] bool started() const {
const auto step = _step.loadAcquire();
return (step == kWaitingForFirstFrameStep) || (step >= 0);
}
bool ready() const;
[[nodiscard]] bool ready() const;
crl::time getPositionMs() const;
crl::time getDurationMs() const;
[[nodiscard]] crl::time getPositionMs() const;
[[nodiscard]] crl::time getDurationMs() const;
void pauseResumeVideo();
void stop();
@ -217,62 +217,6 @@ inline ReaderPointer MakeReader(Args&&... args) {
return ReaderPointer(new Reader(std::forward<Args>(args)...));
}
enum class ProcessResult {
Error,
Started,
Finished,
Paused,
Repaint,
CopyFrame,
Wait,
};
class Manager : public QObject {
public:
explicit Manager(QThread *thread);
~Manager();
int loadLevel() const {
return _loadLevel;
}
void append(Reader *reader, const Core::FileLocation &location, const QByteArray &data);
void start(Reader *reader);
void update(Reader *reader);
void stop(Reader *reader);
bool carries(Reader *reader) const;
private:
void process();
void finish();
void callback(Reader *reader, Notification notification);
void clear();
QAtomicInt _loadLevel;
using ReaderPointers = QMap<Reader*, QAtomicInt>;
ReaderPointers _readerPointers;
mutable QMutex _readerPointersMutex;
ReaderPointers::const_iterator constUnsafeFindReaderPointer(ReaderPrivate *reader) const;
ReaderPointers::iterator unsafeFindReaderPointer(ReaderPrivate *reader);
bool handleProcessResult(ReaderPrivate *reader, ProcessResult result, crl::time ms);
enum ResultHandleState {
ResultHandleRemove,
ResultHandleStop,
ResultHandleContinue,
};
ResultHandleState handleResult(ReaderPrivate *reader, ProcessResult result, crl::time ms);
using Readers = QMap<ReaderPrivate*, crl::time>;
Readers _readers;
QTimer _timer;
QThread *_processingInThread = nullptr;
bool _needReProcess = false;
};
[[nodiscard]] Ui::PreparedFileInformation::Video PrepareForSending(
const QString &fname,
const QByteArray &data);

View file

@ -1910,12 +1910,10 @@ void Gif::clipCallback(Media::Clip::Notification notification) {
} else {
auto height = st::inlineMediaHeight;
auto frame = countFrameSize();
_gif->start(
frame.width(),
frame.height(),
_width,
height,
ImageRoundRadius::None, RectPart::None);
_gif->start({
.frame = countFrameSize(),
.outer = { _width, st::inlineMediaHeight },
});
}
} else if (_gif->autoPausedGif()
&& !delegate()->itemVisible(this)) {
@ -1997,26 +1995,20 @@ void Gif::paint(
}
const auto radial = isRadialAnimation();
int32 height = st::inlineMediaHeight;
QSize frame = countFrameSize();
QRect r(0, 0, _width, height);
const auto frame = countFrameSize();
const auto r = QRect(0, 0, _width, st::inlineMediaHeight);
if (animating) {
const auto pixmap = _gif->current(
frame.width(),
frame.height(),
_width,
height,
ImageRoundRadius::None,
RectPart::None,
/*context->paused ? 0 : */context->ms);
const auto pixmap = _gif->current({
.frame = frame,
.outer = r.size(),
}, /*context->paused ? 0 : */context->ms);
if (_thumb.isNull()) {
_thumb = pixmap;
_thumbGood = true;
}
p.drawPixmap(r.topLeft(), pixmap);
} else {
prepareThumbnail({ _width, height }, frame);
prepareThumbnail(r.size(), frame);
if (_thumb.isNull()) {
p.fillRect(r, st::overviewPhotoBg);
} else {
@ -2044,7 +2036,11 @@ void Gif::paint(
return &st::historyFileInDownload;
}();
const auto size = st::overviewVideoRadialSize;
QRect inner((_width - size) / 2, (height - size) / 2, size, size);
QRect inner(
(r.width() - size) / 2,
(r.height() - size) / 2,
size,
size);
icon->paintInCenter(p, inner);
if (radial) {
p.setOpacity(1);

View file

@ -74,16 +74,10 @@ bool SingleMediaPreview::drawBackground() const {
bool SingleMediaPreview::tryPaintAnimation(Painter &p) {
if (_gifPreview && _gifPreview->started()) {
auto s = QSize(previewWidth(), previewHeight());
auto paused = _gifPaused();
auto frame = _gifPreview->current(
s.width(),
s.height(),
s.width(),
s.height(),
ImageRoundRadius::None,
RectPart::None,
paused ? 0 : crl::now());
const auto paused = _gifPaused();
const auto frame = _gifPreview->current({
.frame = QSize(previewWidth(), previewHeight()),
}, paused ? 0 : crl::now());
p.drawPixmap(previewLeft(), previewTop(), frame);
return true;
} else if (_lottiePreview && _lottiePreview->ready()) {
@ -139,14 +133,9 @@ void SingleMediaPreview::clipCallback(
}
if (_gifPreview && _gifPreview->ready() && !_gifPreview->started()) {
const auto s = QSize(previewWidth(), previewHeight());
_gifPreview->start(
s.width(),
s.height(),
s.width(),
s.height(),
ImageRoundRadius::None,
RectPart::None);
_gifPreview->start({
.frame = QSize(previewWidth(), previewHeight()),
});
}
update();

View file

@ -285,9 +285,11 @@ QPixmap MediaPreviewWidget::currentImage() const {
? _gif
: _gifThumbnail;
if (gif && gif->started()) {
auto s = currentDimensions();
auto paused = _controller->isGifPausedAtLeastFor(Window::GifPauseReason::MediaPreview);
return gif->current(s.width(), s.height(), s.width(), s.height(), ImageRoundRadius::None, RectPart::None, paused ? 0 : crl::now());
const auto paused = _controller->isGifPausedAtLeastFor(
Window::GifPauseReason::MediaPreview);
return gif->current(
{ .frame = currentDimensions() },
paused ? 0 : crl::now());
}
if (_cacheStatus != CacheThumbLoaded
&& _document->hasThumbnail()) {
@ -335,14 +337,7 @@ QPixmap MediaPreviewWidget::currentImage() const {
void MediaPreviewWidget::startGifAnimation(
const Media::Clip::ReaderPointer &gif) {
const auto s = currentDimensions();
gif->start(
s.width(),
s.height(),
s.width(),
s.height(),
ImageRoundRadius::None,
RectPart::None);
gif->start({ .frame = currentDimensions() });
}
void MediaPreviewWidget::validateGifAnimation() {