/* 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/stories/media_stories_sibling.h" #include "base/weak_ptr.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_file_origin.h" #include "data/data_peer.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "media/stories/media_stories_controller.h" #include "media/stories/media_stories_view.h" #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" #include "ui/painter.h" #include "styles/style_media_view.h" namespace Media::Stories { namespace { constexpr auto kGoodFadeDuration = crl::time(200); constexpr auto kSiblingFade = 0.5; constexpr auto kSiblingFadeOver = 0.4; constexpr auto kSiblingNameOpacity = 0.8; constexpr auto kSiblingNameOpacityOver = 1.; constexpr auto kSiblingScaleOver = 0.05; [[nodiscard]] StoryId LookupShownId( const Data::StoriesSource &source, StoryId suggestedId) { const auto i = suggestedId ? source.ids.lower_bound(Data::StoryIdDates{ suggestedId }) : end(source.ids); return (i != end(source.ids) && i->id == suggestedId) ? suggestedId : source.toOpen().id; } } // namespace class Sibling::Loader { public: virtual ~Loader() = default; virtual QImage blurred() = 0; virtual QImage good() = 0; }; class Sibling::LoaderPhoto final : public Sibling::Loader { public: LoaderPhoto( not_null photo, Data::FileOrigin origin, Fn update); QImage blurred() override; QImage good() override; private: const not_null _photo; const Fn _update; std::shared_ptr _media; rpl::lifetime _waitingLoading; }; class Sibling::LoaderVideo final : public Sibling::Loader , public base::has_weak_ptr { public: LoaderVideo( not_null video, Data::FileOrigin origin, Fn update); QImage blurred() override; QImage good() override; private: void waitForGoodThumbnail(); bool updateAfterGoodCheck(); void createStreamedPlayer(); void streamedFailed(); const not_null _video; const Data::FileOrigin _origin; const Fn _update; std::shared_ptr _media; std::unique_ptr _streamed; rpl::lifetime _waitingGoodGeneration; bool _checkingGoodInCache = false; bool _failed = false; }; Sibling::LoaderPhoto::LoaderPhoto( not_null photo, Data::FileOrigin origin, Fn update) : _photo(photo) , _update(std::move(update)) , _media(_photo->createMediaView()) { _photo->load(origin, LoadFromCloudOrLocal, true); } QImage Sibling::LoaderPhoto::blurred() { if (const auto image = _media->thumbnailInline()) { return image->original(); } const auto ratio = style::DevicePixelRatio(); auto result = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied); result.fill(Qt::black); result.setDevicePixelRatio(ratio); return result; } QImage Sibling::LoaderPhoto::good() { if (const auto image = _media->image(Data::PhotoSize::Large)) { return image->original(); } else if (!_waitingLoading) { _photo->session().downloaderTaskFinished( ) | rpl::start_with_next([=] { if (_media->loaded()) { _update(); } }, _waitingLoading); } return QImage(); } Sibling::LoaderVideo::LoaderVideo( not_null video, Data::FileOrigin origin, Fn update) : _video(video) , _origin(origin) , _update(std::move(update)) , _media(_video->createMediaView()) { _media->goodThumbnailWanted(); } QImage Sibling::LoaderVideo::blurred() { if (const auto image = _media->thumbnailInline()) { return image->original(); } const auto ratio = style::DevicePixelRatio(); auto result = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied); result.fill(Qt::black); result.setDevicePixelRatio(ratio); return result; } QImage Sibling::LoaderVideo::good() { if (const auto image = _media->goodThumbnail()) { return image->original(); } else if (!_video->goodThumbnailChecked() && !_video->goodThumbnailNoData()) { if (!_checkingGoodInCache) { waitForGoodThumbnail(); } } else if (_failed) { return QImage(); } else if (!_streamed) { createStreamedPlayer(); } else if (_streamed->ready()) { return _streamed->info().video.cover; } return QImage(); } void Sibling::LoaderVideo::createStreamedPlayer() { _streamed = std::make_unique( _video, _origin, [] {}); // waitingCallback _streamed->lockPlayer(); _streamed->player().updates( ) | rpl::start_with_next_error([=](Streaming::Update &&update) { v::match(update.data, [&](Streaming::Information &update) { _update(); }, [](const auto &update) { }); }, [=](Streaming::Error &&error) { streamedFailed(); }, _streamed->lifetime()); if (_streamed->ready()) { _update(); } else if (!_streamed->valid()) { streamedFailed(); } else if (!_streamed->player().active() && !_streamed->player().finished()) { _streamed->play({ .mode = Streaming::Mode::Video, }); _streamed->pause(); } } void Sibling::LoaderVideo::streamedFailed() { _failed = true; _streamed = nullptr; _update(); } void Sibling::LoaderVideo::waitForGoodThumbnail() { _checkingGoodInCache = true; const auto weak = make_weak(this); _video->owner().cache().get({}, [=](const auto &) { crl::on_main([=] { if (const auto strong = weak.get()) { if (!strong->updateAfterGoodCheck()) { strong->_video->session().downloaderTaskFinished( ) | rpl::start_with_next([=] { strong->updateAfterGoodCheck(); }, strong->_waitingGoodGeneration); } } }); }); } bool Sibling::LoaderVideo::updateAfterGoodCheck() { if (!_video->goodThumbnailChecked() && !_video->goodThumbnailNoData()) { return false; } _checkingGoodInCache = false; _waitingGoodGeneration.destroy(); _update(); return true; } Sibling::Sibling( not_null controller, const Data::StoriesSource &source, StoryId suggestedId) : _controller(controller) , _id{ source.peer->id, LookupShownId(source, suggestedId) } , _peer(source.peer) { checkStory(); _goodShown.stop(); } Sibling::~Sibling() = default; void Sibling::checkStory() { const auto maybeStory = _peer->owner().stories().lookup(_id); if (!maybeStory) { if (_blurred.isNull()) { setBlackThumbnail(); if (maybeStory.error() == Data::NoStory::Unknown) { _peer->owner().stories().resolve(_id, crl::guard(this, [=] { checkStory(); })); } } return; } const auto story = *maybeStory; const auto origin = Data::FileOrigin(); v::match(story->media().data, [&](not_null photo) { _loader = std::make_unique(photo, origin, [=] { check(); }); }, [&](not_null document) { _loader = std::make_unique(document, origin, [=] { check(); }); }, [&](v::null_t) { _loader = nullptr; }); if (!_loader) { setBlackThumbnail(); return; } _blurred = _loader->blurred(); check(); } void Sibling::setBlackThumbnail() { _blurred = QImage( st::storiesMaxSize, QImage::Format_ARGB32_Premultiplied); _blurred.fill(Qt::black); } FullStoryId Sibling::shownId() const { return _id; } not_null Sibling::peer() const { return _peer; } bool Sibling::shows( const Data::StoriesSource &source, StoryId suggestedId) const { const auto fullId = FullStoryId{ source.peer->id, LookupShownId(source, suggestedId), }; return (_id == fullId); } SiblingView Sibling::view(const SiblingLayout &layout, float64 over) { const auto name = nameImage(layout); return { .image = _good.isNull() ? _blurred : _good, .layout = { .geometry = layout.geometry, .fade = kSiblingFade * (1 - over) + kSiblingFadeOver * over, .radius = st::storiesRadius, }, .userpic = userpicImage(layout), .userpicPosition = layout.userpic.topLeft(), .name = name, .namePosition = namePosition(layout, name), .nameOpacity = (kSiblingNameOpacity * (1 - over) + kSiblingNameOpacityOver * over), .scale = 1. + (over * kSiblingScaleOver), }; } QImage Sibling::userpicImage(const SiblingLayout &layout) { const auto ratio = style::DevicePixelRatio(); const auto size = layout.userpic.width() * ratio; const auto key = _peer->userpicUniqueKey(_userpicView); if (_userpicImage.width() != size || _userpicKey != key) { _userpicKey = key; _userpicImage = PeerData::GenerateUserpicImage( _peer, _userpicView, size); _userpicImage.setDevicePixelRatio(ratio); } return _userpicImage; } QImage Sibling::nameImage(const SiblingLayout &layout) { if (_nameFontSize != layout.nameFontSize) { _nameFontSize = layout.nameFontSize; const auto family = 0; // Default font family. const auto font = style::font( _nameFontSize, style::FontFlag::Semibold, family); _name.reset(); _nameStyle = std::make_unique(style::TextStyle{ .font = font, }); }; const auto text = _peer->isSelf() ? tr::lng_stories_my_name(tr::now) : _peer->shortName(); if (_nameText != text) { _name.reset(); _nameText = text; } if (!_name) { _nameAvailableWidth = 0; _name.emplace(*_nameStyle, _nameText); } const auto available = layout.nameBoundingRect.width(); const auto wasCut = (_nameAvailableWidth < _name->maxWidth()); const auto nowCut = (available < _name->maxWidth()); if (_nameImage.isNull() || _nameAvailableWidth != layout.nameBoundingRect.width()) { _nameAvailableWidth = layout.nameBoundingRect.width(); if (_nameImage.isNull() || nowCut || wasCut) { const auto w = std::min(_nameAvailableWidth, _name->maxWidth()); const auto h = _nameStyle->font->height; const auto ratio = style::DevicePixelRatio(); _nameImage = QImage( QSize(w, h) * ratio, QImage::Format_ARGB32_Premultiplied); _nameImage.setDevicePixelRatio(ratio); _nameImage.fill(Qt::transparent); auto p = Painter(&_nameImage); auto hq = PainterHighQualityEnabler(p); p.setFont(_nameStyle->font); p.setPen(Qt::white); _name->drawLeftElided(p, 0, 0, w, w); } } return _nameImage; } QPoint Sibling::namePosition( const SiblingLayout &layout, const QImage &image) const { const auto size = image.size() / image.devicePixelRatio(); const auto width = size.width(); const auto bounding = layout.nameBoundingRect; const auto left = layout.geometry.x() + (layout.geometry.width() - width) / 2; const auto top = bounding.y() + bounding.height() - size.height(); if (left < bounding.x()) { return { bounding.x(), top }; } else if (left + width > bounding.x() + bounding.width()) { return { bounding.x() + bounding.width() - width, top }; } return { left, top }; } void Sibling::check() { Expects(_loader != nullptr); auto good = _loader->good(); if (good.isNull()) { return; } _loader = nullptr; _good = std::move(good); _goodShown.start([=] { _controller->repaintSibling(this); }, 0., 1., kGoodFadeDuration, anim::linear); } } // namespace Media::Stories