Track shown sponsored in video.

This commit is contained in:
John Preston 2025-07-08 20:28:18 +04:00
parent e5ca9e4c39
commit 284cbda7c0
8 changed files with 345 additions and 32 deletions

View file

@ -1257,6 +1257,8 @@ PRIVATE
media/view/media_view_playback_controls.h
media/view/media_view_playback_progress.cpp
media/view/media_view_playback_progress.h
media/view/media_view_playback_sponsored.cpp
media/view/media_view_playback_sponsored.h
media/system_media_controls_manager.h
media/system_media_controls_manager.cpp
menu/menu_antispam_validator.cpp

View file

@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data {
namespace {
constexpr auto kMs = crl::time(1000);
constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000);
const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01));
@ -288,7 +289,7 @@ void SponsoredMessages::request(not_null<History*> history, Fn<void()> done) {
}).send();
}
void SponsoredMessages::request(
void SponsoredMessages::requestForVideo(
not_null<HistoryItem*> item,
Fn<void(SponsoredForVideo)> done) {
Expects(done != nullptr);
@ -297,14 +298,23 @@ void SponsoredMessages::request(
done({});
return;
}
const auto id = item->fullId();
auto &request = _requestsForVideo[id];
const auto peer = item->history()->peer;
auto &request = _requestsForVideo[peer];
if (TooEarlyForRequest(request.lastReceived)) {
auto prepared = prepareForVideo(peer);
if (prepared.list.empty()
|| prepared.state.itemIndex < prepared.list.size()
|| prepared.state.leftTillShow > 0) {
done(std::move(prepared));
return;
}
}
request.callbacks.push_back(std::move(done));
if (request.requestId) {
done(prepareForVideo(id));
return;
}
{
const auto it = _dataForVideo.find(id);
const auto it = _dataForVideo.find(peer);
if (it != end(_dataForVideo)) {
auto &list = it->second;
// Don't rebuild currently displayed messages.
@ -316,21 +326,41 @@ void SponsoredMessages::request(
}
}
}
const auto finish = [=] {
const auto i = _requestsForVideo.find(peer);
if (i != end(_requestsForVideo)) {
for (const auto &callback : base::take(i->second.callbacks)) {
callback(prepareForVideo(peer));
}
}
};
using Flag = MTPmessages_GetSponsoredMessages::Flag;
request.requestId = _session->api().request(
MTPmessages_GetSponsoredMessages(
MTP_flags(Flag::f_msg_id),
item->history()->peer->input,
peer->input,
MTP_int(item->id.bare))
).done([=](const MTPmessages_sponsoredMessages &result) {
parse(id, result);
done(prepareForVideo(id));
parseForVideo(peer, result);
finish();
}).fail([=] {
_requestsForVideo.remove(id);
done({});
_requestsForVideo.remove(peer);
finish();
}).send();
}
void SponsoredMessages::updateForVideo(
FullMsgId itemId,
SponsoredForVideoState state) {
if (state.initial()) {
return;
}
const auto i = _dataForVideo.find(_session->data().peer(itemId.peer));
if (i != end(_dataForVideo)) {
i->second.state = state;
}
}
void SponsoredMessages::parse(
not_null<History*> history,
const MTPmessages_sponsoredMessages &list) {
@ -366,10 +396,10 @@ void SponsoredMessages::parse(
});
}
void SponsoredMessages::parse(
FullMsgId itemId,
void SponsoredMessages::parseForVideo(
not_null<PeerData*> peer,
const MTPmessages_sponsoredMessages &list) {
auto &request = _requestsForVideo[itemId];
auto &request = _requestsForVideo[peer];
request.lastReceived = crl::now();
request.requestId = 0;
if (!_clearTimer.isActive()) {
@ -380,24 +410,25 @@ void SponsoredMessages::parse(
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
const auto history = _session->data().history(itemId.peer);
const auto history = _session->data().history(peer);
const auto &messages = data.vmessages().v;
auto &list = _dataForVideo.emplace(itemId).first->second;
auto &list = _dataForVideo.emplace(peer).first->second;
list.entries.clear();
list.received = crl::now();
list.startDelay = data.vstart_delay().value_or_empty();
list.betweenDelay = data.vbetween_delay().value_or_empty();
list.startDelay = 2000;AssertIsDebug()// data.vstart_delay().value_or_empty() * kMs;
list.betweenDelay = 3000;//data.vbetween_delay().value_or_empty() * kMs;
for (const auto &message : messages) {
append([=] {
return &_dataForVideo[itemId].entries;
return &_dataForVideo[peer].entries;
}, history, message);
}
}, [](const MTPDmessages_sponsoredMessagesEmpty &) {
});
}
SponsoredForVideo SponsoredMessages::prepareForVideo(FullMsgId itemId) {
const auto i = _dataForVideo.find(itemId);
SponsoredForVideo SponsoredMessages::prepareForVideo(
not_null<PeerData*> peer) {
const auto i = _dataForVideo.find(peer);
if (i == end(_dataForVideo) || i->second.entries.empty()) {
return {};
}
@ -407,6 +438,7 @@ SponsoredForVideo SponsoredMessages::prepareForVideo(FullMsgId itemId) {
) | ranges::to_vector,
.startDelay = i->second.startDelay,
.betweenDelay = i->second.betweenDelay,
.state = i->second.state,
};
}
@ -535,6 +567,8 @@ void SponsoredMessages::append(
.link = from.link,
.sponsorInfo = std::move(sponsorInfo),
.additionalInfo = std::move(additionalInfo),
.durationMin = 3000, AssertIsDebug()//data.vmin_display_duration().value_or_empty() * kMs,
.durationMax = 5000,//data.vmax_display_duration().value_or_empty() * kMs,
};
const auto itemId = FullMsgId(
history->peer->id,

View file

@ -71,6 +71,8 @@ struct SponsoredMessage {
QString link;
TextWithEntities sponsorInfo;
TextWithEntities additionalInfo;
crl::time durationMin = 0;
crl::time durationMax = 0;
};
struct SponsoredMessageDetails {
@ -92,10 +94,21 @@ struct SponsoredReportAction {
Fn<void(Data::SponsoredReportResult)>)> callback;
};
struct SponsoredForVideoState {
int itemIndex = 0;
crl::time leftTillShow = 0;
[[nodiscard]] bool initial() const {
return !itemIndex && !leftTillShow;
}
};
struct SponsoredForVideo {
std::vector<SponsoredMessage> list;
crl::time startDelay = 0;
crl::time betweenDelay = 0;
SponsoredForVideoState state;
};
class SponsoredMessages final {
@ -120,9 +133,12 @@ public:
[[nodiscard]] bool canHaveFor(not_null<HistoryItem*> item) const;
[[nodiscard]] bool isTopBarFor(not_null<History*> history) const;
void request(not_null<History*> history, Fn<void()> done);
void request(
void requestForVideo(
not_null<HistoryItem*> item,
Fn<void(SponsoredForVideo)> done);
void updateForVideo(
FullMsgId itemId,
SponsoredForVideoState state);
void clearItems(not_null<History*> history);
[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;
[[nodiscard]] Details lookupDetails(
@ -181,23 +197,30 @@ private:
crl::time received = 0;
crl::time startDelay = 0;
crl::time betweenDelay = 0;
SponsoredForVideoState state;
};
struct Request {
mtpRequestId requestId = 0;
crl::time lastReceived = 0;
};
struct RequestForVideo {
std::vector<Fn<void(SponsoredForVideo)>> callbacks;
mtpRequestId requestId = 0;
crl::time lastReceived = 0;
};
void parse(
not_null<History*> history,
const MTPmessages_sponsoredMessages &list);
void parse(
FullMsgId itemId,
void parseForVideo(
not_null<PeerData*> peer,
const MTPmessages_sponsoredMessages &list);
void append(
Fn<not_null<std::vector<Entry>*>()> entries,
not_null<History*> history,
const MTPSponsoredMessage &message);
[[nodiscard]] SponsoredForVideo prepareForVideo(FullMsgId itemId);
[[nodiscard]] SponsoredForVideo prepareForVideo(
not_null<PeerData*> peer);
void clearOldRequests();
const Entry *find(const FullMsgId &fullId) const;
@ -209,8 +232,8 @@ private:
base::flat_map<not_null<History*>, Request> _requests;
base::flat_map<RandomId, Request> _viewRequests;
base::flat_map<FullMsgId, ListForVideo> _dataForVideo;
base::flat_map<FullMsgId, Request> _requestsForVideo;
base::flat_map<not_null<PeerData*>, ListForVideo> _dataForVideo;
base::flat_map<not_null<PeerData*>, RequestForVideo> _requestsForVideo;
rpl::event_stream<FullMsgId> _itemRemoved;

View file

@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/view/media_view_pip.h"
#include "media/view/media_view_overlay_raster.h"
#include "media/view/media_view_overlay_opengl.h"
#include "media/view/media_view_playback_sponsored.h"
#include "media/stories/media_stories_share.h"
#include "media/stories/media_stories_view.h"
#include "media/streaming/media_streaming_document.h"
@ -335,6 +336,7 @@ struct OverlayWidget::Streamed {
Streaming::Instance instance;
std::unique_ptr<PlaybackControls> controls;
std::unique_ptr<PlaybackSponsored> sponsored;
std::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker;
bool ready = false;
@ -4069,6 +4071,9 @@ void OverlayWidget::initStreamingThumbnail() {
void OverlayWidget::streamingReady(Streaming::Information &&info) {
_streamed->ready = true;
if (const auto sponsored = _streamed->sponsored.get()) {
sponsored->start();
}
if (videoShown()) {
applyVideoSize();
_streamedQualityChangeFrame = QImage();
@ -4090,6 +4095,7 @@ void OverlayWidget::applyVideoSize() {
bool OverlayWidget::createStreamingObjects() {
Expects(_photo || _document);
Expects(!_streamed);
const auto origin = fileOrigin();
const auto callback = [=] { waitingAnimationCallback(); };
@ -4122,6 +4128,15 @@ bool OverlayWidget::createStreamingObjects() {
_body,
static_cast<PlaybackControls::Delegate*>(this));
_streamed->controls->show();
_streamed->sponsored = PlaybackSponsored::Has(_message)
? std::make_unique<PlaybackSponsored>(_body, _message)
: nullptr;
if (const auto sponsored = _streamed->sponsored.get()) {
_layerBg->layerShownValue(
) | rpl::start_with_next([=](bool shown) {
sponsored->setPaused(shown);
}, sponsored->lifetime());
}
refreshClipControllerGeometry();
}
return true;

View file

@ -78,6 +78,7 @@ struct ContentLayout;
namespace Media::View {
class PlaybackSponsored;
class GroupThumbs;
class Pip;

View file

@ -19,14 +19,13 @@ class MediaSlider;
class PopupMenu;
} // namespace Ui
namespace Media {
namespace Player {
namespace Media::Player {
struct TrackState;
class SettingsButton;
class SpeedController;
} // namespace Player
} // namespace Media::Player
namespace View {
namespace Media::View {
class PlaybackProgress;
@ -131,5 +130,4 @@ private:
};
} // namespace View
} // namespace Media
} // namespace Media::View

View file

@ -0,0 +1,178 @@
/*
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/view/media_view_playback_sponsored.h"
#include "data/components/sponsored_messages.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "main/main_session.h"
namespace Media::View {
namespace {
constexpr auto kStartDelayMin = crl::time(1000);
constexpr auto kDurationMin = 5 * crl::time(1000);
} // namespace
PlaybackSponsored::PlaybackSponsored(
QWidget *parent,
not_null<HistoryItem*> item)
: _parent(parent)
, _session(&item->history()->session())
, _itemId(item->fullId())
, _timer([=] { update(); }) {
_session->sponsoredMessages().requestForVideo(item, crl::guard(this, [=](
Data::SponsoredForVideo data) {
if (data.list.empty()) {
return;
}
_data = std::move(data);
if (_data->state.initial()
|| (_data->state.itemIndex > _data->list.size())
|| (_data->state.itemIndex == _data->list.size()
&& _data->state.leftTillShow <= 0)) {
_data->state.itemIndex = 0;
_data->state.leftTillShow = std::max(
_data->startDelay,
kStartDelayMin);
}
update();
}));
}
PlaybackSponsored::~PlaybackSponsored() {
saveState();
}
void PlaybackSponsored::start() {
_started = true;
if (!_paused) {
_start = crl::now();
update();
}
}
void PlaybackSponsored::setPaused(bool paused) {
if (_paused == paused) {
return;
}
_paused = paused;
if (!_started) {
return;
} else if (_paused) {
update();
const auto state = computeState();
_start = 0;
_timer.cancel();
} else {
_start = crl::now();
update();
}
}
void PlaybackSponsored::finish() {
_timer.cancel();
if (_data) {
saveState();
_data = std::nullopt;
}
}
void PlaybackSponsored::update() {
if (!_data || !_start) {
return;
}
const auto [now, state] = computeState();
const auto message = (_data->state.itemIndex < _data->list.size())
? &_data->list[state.itemIndex]
: nullptr;
const auto duration = message
? std::max(
message->durationMin + kDurationMin,
message->durationMax)
: crl::time(0);
if (_data->state.leftTillShow > 0 && state.leftTillShow <= 0) {
_data->state.leftTillShow = 0;
if (duration) {
show(*message);
_start = now;
_timer.callOnce(duration);
saveState();
} else {
finish();
}
} else if (_data->state.leftTillShow <= 0
&& state.leftTillShow <= -duration) {
hide();
++_data->state.itemIndex;
_data->state.leftTillShow = std::max(
_data->betweenDelay,
kStartDelayMin);
_start = now;
_timer.callOnce(_data->state.leftTillShow);
saveState();
} else {
if (state.leftTillShow <= 0 && duration && !_widget) {
show(*message);
}
_data->state = state;
_timer.callOnce((state.leftTillShow > 0)
? state.leftTillShow
: (state.leftTillShow + duration));
}
}
void PlaybackSponsored::show(const Data::SponsoredMessage &data) {
_widget = std::make_unique<Ui::RpWidget>(_parent);
_widget->setGeometry(_parent->rect());
_widget->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(_widget.get());
p.fillRect(_widget->rect(), QColor(0, 128, 0, 128));
}, _widget->lifetime());
_widget->show();
}
void PlaybackSponsored::hide() {
_widget = nullptr;
}
void PlaybackSponsored::saveState() {
_session->sponsoredMessages().updateForVideo(
_itemId,
computeState().data);
}
PlaybackSponsored::State PlaybackSponsored::computeState() const {
auto result = State{ crl::now() };
if (!_data) {
return result;
}
result.data = _data->state;
if (!_start) {
return result;
}
const auto elapsed = result.now - _start;
result.data.leftTillShow -= elapsed;
return result;
}
rpl::lifetime &PlaybackSponsored::lifetime() {
return _lifetime;
}
bool PlaybackSponsored::Has(HistoryItem *item) {
return item
&& item->history()->session().sponsoredMessages().canHaveFor(item);
}
} // namespace Media::View

View file

@ -0,0 +1,62 @@
/*
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 "base/timer.h"
#include "base/weak_ptr.h"
#include "data/components/sponsored_messages.h"
namespace Main {
class Session;
} // namespace Main
namespace Media::View {
class PlaybackSponsored final : public base::has_weak_ptr {
public:
PlaybackSponsored(QWidget *parent, not_null<HistoryItem*> item);
~PlaybackSponsored();
void start();
void setPaused(bool paused);
[[nodiscard]] rpl::lifetime &lifetime();
[[nodiscard]] static bool Has(HistoryItem *item);
private:
struct State {
crl::time now = 0;
Data::SponsoredForVideoState data;
};
void update();
void finish();
void show(const Data::SponsoredMessage &data);
void hide();
[[nodiscard]] State computeState() const;
void saveState();
const not_null<QWidget*> _parent;
const not_null<Main::Session*> _session;
const FullMsgId _itemId;
std::unique_ptr<Ui::RpWidget> _widget;
crl::time _start = 0;
bool _started = false;
bool _paused = false;
base::Timer _timer;
std::optional<Data::SponsoredForVideo> _data;
rpl::lifetime _lifetime;
};
} // namespace Media::View