2019-06-28 13:24:33 +00:00
|
|
|
/*
|
|
|
|
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 "lottie/lottie_multi_player.h"
|
|
|
|
|
|
|
|
#include "lottie/lottie_frame_renderer.h"
|
|
|
|
#include "lottie/lottie_animation.h"
|
2019-06-29 12:24:46 +00:00
|
|
|
#include "logs.h"
|
2019-06-28 13:24:33 +00:00
|
|
|
|
|
|
|
#include <range/v3/algorithm/remove.hpp>
|
|
|
|
|
|
|
|
namespace Lottie {
|
|
|
|
|
|
|
|
MultiPlayer::MultiPlayer(std::shared_ptr<FrameRenderer> renderer)
|
2019-06-29 12:24:46 +00:00
|
|
|
: _timer([=] { checkNextFrameRender(); })
|
|
|
|
, _renderer(renderer ? std::move(renderer) : FrameRenderer::Instance()) {
|
2019-06-30 13:19:57 +00:00
|
|
|
crl::on_main_update_requests(
|
|
|
|
) | rpl::start_with_next([=] {
|
|
|
|
checkStep();
|
|
|
|
}, _lifetime);
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
MultiPlayer::~MultiPlayer() {
|
|
|
|
for (const auto &[animation, state] : _active) {
|
|
|
|
_renderer->remove(state);
|
|
|
|
}
|
2019-07-01 11:20:53 +00:00
|
|
|
for (const auto &[animation, info] : _paused) {
|
|
|
|
_renderer->remove(info.state);
|
|
|
|
}
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
not_null<Animation*> MultiPlayer::append(
|
|
|
|
FnMut<void(FnMut<void(QByteArray &&cached)>)> get, // Main thread.
|
|
|
|
FnMut<void(QByteArray &&cached)> put, // Unknown thread.
|
|
|
|
const QByteArray &content,
|
|
|
|
const FrameRequest &request) {
|
|
|
|
_animations.push_back(std::make_unique<Animation>(
|
|
|
|
this,
|
|
|
|
std::move(get),
|
|
|
|
std::move(put),
|
|
|
|
content,
|
|
|
|
request));
|
|
|
|
return _animations.back().get();
|
|
|
|
}
|
|
|
|
|
|
|
|
not_null<Animation*> MultiPlayer::append(
|
|
|
|
const QByteArray &content,
|
|
|
|
const FrameRequest &request) {
|
|
|
|
_animations.push_back(std::make_unique<Animation>(
|
|
|
|
this,
|
|
|
|
content,
|
|
|
|
request));
|
|
|
|
return _animations.back().get();
|
|
|
|
}
|
|
|
|
|
2019-07-01 11:20:53 +00:00
|
|
|
void MultiPlayer::startAtRightTime(std::unique_ptr<SharedState> state) {
|
|
|
|
if (_started == kTimeUnknown) {
|
|
|
|
_started = crl::now();
|
|
|
|
_lastSyncTime = kTimeUnknown;
|
|
|
|
_delay = 0;
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
2019-07-01 11:20:53 +00:00
|
|
|
const auto lastSyncTime = (_lastSyncTime != kTimeUnknown)
|
|
|
|
? _lastSyncTime
|
|
|
|
: _started;
|
|
|
|
const auto frameIndex = countFrameIndex(
|
|
|
|
state.get(),
|
|
|
|
lastSyncTime,
|
|
|
|
_delay);
|
|
|
|
state->start(this, _started, _delay, frameIndex);
|
|
|
|
|
|
|
|
_renderer->append(std::move(state));
|
|
|
|
}
|
|
|
|
|
|
|
|
int MultiPlayer::countFrameIndex(
|
|
|
|
not_null<SharedState*> state,
|
|
|
|
crl::time time,
|
|
|
|
crl::time delay) const {
|
|
|
|
Expects(time != kTimeUnknown);
|
2019-06-28 13:24:33 +00:00
|
|
|
|
|
|
|
const auto rate = state->information().frameRate;
|
|
|
|
Assert(rate != 0);
|
|
|
|
|
2019-07-01 11:20:53 +00:00
|
|
|
const auto framesTime = time - _started - delay;
|
|
|
|
return ((framesTime + 1) * rate - 1) / 1000;
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::start(
|
|
|
|
not_null<Animation*> animation,
|
|
|
|
std::unique_ptr<SharedState> state) {
|
|
|
|
Expects(state != nullptr);
|
|
|
|
|
2019-07-01 11:20:53 +00:00
|
|
|
const auto paused = _pausedBeforeStart.remove(animation);
|
|
|
|
auto info = StartingInfo{ std::move(state), paused };
|
|
|
|
if (_active.empty()
|
|
|
|
|| (_lastSyncTime == kTimeUnknown
|
|
|
|
&& _nextFrameTime == kTimeUnknown)) {
|
|
|
|
startBeforeLifeCycle(animation, std::move(info));
|
2019-06-30 13:19:57 +00:00
|
|
|
} else {
|
|
|
|
// We always try to mark as shown at the same time, so we start a new
|
|
|
|
// animation at the same time we mark all existing as shown.
|
2019-07-01 11:20:53 +00:00
|
|
|
_pendingToStart.emplace(animation, std::move(info));
|
2019-06-30 13:19:57 +00:00
|
|
|
}
|
2019-07-01 11:20:53 +00:00
|
|
|
_updates.fire({});
|
2019-06-30 13:19:57 +00:00
|
|
|
}
|
|
|
|
|
2019-07-01 11:20:53 +00:00
|
|
|
void MultiPlayer::startBeforeLifeCycle(
|
|
|
|
not_null<Animation*> animation,
|
|
|
|
StartingInfo &&info) {
|
|
|
|
_active.emplace(animation, info.state.get());
|
|
|
|
startAtRightTime(std::move(info.state));
|
|
|
|
if (info.paused) {
|
|
|
|
_pendingPause.emplace(animation);
|
2019-06-30 13:19:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-01 11:20:53 +00:00
|
|
|
void MultiPlayer::startInsideLifeCycle(
|
2019-06-30 13:19:57 +00:00
|
|
|
not_null<Animation*> animation,
|
2019-07-01 11:20:53 +00:00
|
|
|
StartingInfo &&info) {
|
|
|
|
const auto state = info.state.get();
|
|
|
|
if (info.paused) {
|
|
|
|
_paused.emplace(
|
|
|
|
animation,
|
|
|
|
PausedInfo{ state, _lastSyncTime, _delay });
|
|
|
|
} else {
|
|
|
|
_active.emplace(animation, state);
|
|
|
|
}
|
|
|
|
startAtRightTime(std::move(info.state));
|
|
|
|
}
|
2019-06-30 13:19:57 +00:00
|
|
|
|
2019-07-01 11:20:53 +00:00
|
|
|
void MultiPlayer::processPending() {
|
|
|
|
Expects(_lastSyncTime != kTimeUnknown);
|
2019-06-28 13:24:33 +00:00
|
|
|
|
2019-07-01 11:20:53 +00:00
|
|
|
for (const auto &animation : base::take(_pendingPause)) {
|
|
|
|
pauseAndSaveState(animation);
|
|
|
|
}
|
|
|
|
for (const auto &animation : base::take(_pendingUnpause)) {
|
|
|
|
unpauseAndKeepUp(animation);
|
|
|
|
}
|
|
|
|
for (auto &[animation, info] : base::take(_pendingToStart)) {
|
|
|
|
startInsideLifeCycle(animation, std::move(info));
|
|
|
|
}
|
|
|
|
for (const auto &animation : base::take(_pendingRemove)) {
|
|
|
|
removeNow(animation);
|
|
|
|
}
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::remove(not_null<Animation*> animation) {
|
2019-07-01 11:20:53 +00:00
|
|
|
if (!_active.empty()) {
|
|
|
|
_pendingRemove.emplace(animation);
|
|
|
|
} else {
|
|
|
|
removeNow(animation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::removeNow(not_null<Animation*> animation) {
|
2019-06-28 13:24:33 +00:00
|
|
|
const auto i = _active.find(animation);
|
|
|
|
if (i != end(_active)) {
|
|
|
|
_renderer->remove(i->second);
|
2019-07-01 11:20:53 +00:00
|
|
|
_active.erase(i);
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
2019-07-01 11:20:53 +00:00
|
|
|
const auto j = _paused.find(animation);
|
|
|
|
if (j != end(_paused)) {
|
|
|
|
_renderer->remove(j->second.state);
|
|
|
|
_paused.erase(j);
|
|
|
|
}
|
|
|
|
|
|
|
|
_pendingRemove.remove(animation);
|
|
|
|
_pendingToStart.remove(animation);
|
|
|
|
_pendingPause.remove(animation);
|
|
|
|
_pendingUnpause.remove(animation);
|
|
|
|
_pausedBeforeStart.remove(animation);
|
2019-06-28 13:24:33 +00:00
|
|
|
_animations.erase(
|
|
|
|
ranges::remove(
|
|
|
|
_animations,
|
|
|
|
animation.get(),
|
|
|
|
&std::unique_ptr<Animation>::get),
|
|
|
|
end(_animations));
|
|
|
|
|
|
|
|
if (_active.empty()) {
|
|
|
|
_nextFrameTime = kTimeUnknown;
|
|
|
|
_timer.cancel();
|
2019-07-01 11:20:53 +00:00
|
|
|
if (_paused.empty()) {
|
|
|
|
_started = kTimeUnknown;
|
|
|
|
_lastSyncTime = kTimeUnknown;
|
|
|
|
_delay = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::pause(not_null<Animation*> animation) {
|
|
|
|
if (_active.contains(animation)) {
|
|
|
|
_pendingPause.emplace(animation);
|
|
|
|
} else if (_paused.contains(animation)) {
|
|
|
|
_pendingUnpause.remove(animation);
|
|
|
|
} else if (const auto i = _pendingToStart.find(animation); i != end(_pendingToStart)) {
|
|
|
|
i->second.paused = true;
|
|
|
|
} else {
|
|
|
|
_pausedBeforeStart.emplace(animation);
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-01 11:20:53 +00:00
|
|
|
void MultiPlayer::unpause(not_null<Animation*> animation) {
|
|
|
|
if (const auto i = _paused.find(animation); i != end(_paused)) {
|
|
|
|
if (_active.empty()) {
|
|
|
|
unpauseFirst(animation, i->second.state);
|
|
|
|
_paused.erase(i);
|
|
|
|
} else {
|
|
|
|
_pendingUnpause.emplace(animation);
|
|
|
|
}
|
|
|
|
} else if (_pendingPause.contains(animation)) {
|
|
|
|
_pendingPause.remove(animation);
|
|
|
|
} else {
|
|
|
|
const auto i = _pendingToStart.find(animation);
|
|
|
|
if (i != end(_pendingToStart)) {
|
|
|
|
i->second.paused = false;
|
|
|
|
} else {
|
|
|
|
_pausedBeforeStart.remove(animation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::unpauseFirst(
|
|
|
|
not_null<Animation*> animation,
|
|
|
|
not_null<SharedState*> state) {
|
|
|
|
Expects(_lastSyncTime != kTimeUnknown);
|
|
|
|
|
|
|
|
_active.emplace(animation, state);
|
|
|
|
|
|
|
|
const auto now = crl::now();
|
|
|
|
addTimelineDelay(now - _lastSyncTime);
|
|
|
|
_lastSyncTime = now;
|
|
|
|
|
|
|
|
markFrameShown();
|
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::pauseAndSaveState(not_null<Animation*> animation) {
|
|
|
|
Expects(_lastSyncTime != kTimeUnknown);
|
|
|
|
|
|
|
|
const auto i = _active.find(animation);
|
|
|
|
Assert(i != end(_active));
|
|
|
|
_paused.emplace(
|
|
|
|
animation,
|
|
|
|
PausedInfo{ i->second, _lastSyncTime, _delay });
|
|
|
|
_active.erase(i);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::unpauseAndKeepUp(not_null<Animation*> animation) {
|
|
|
|
Expects(_lastSyncTime != kTimeUnknown);
|
|
|
|
|
|
|
|
const auto i = _paused.find(animation);
|
|
|
|
Assert(i != end(_paused));
|
|
|
|
const auto state = i->second.state;
|
|
|
|
const auto frameIndexAtPaused = countFrameIndex(
|
|
|
|
state,
|
|
|
|
i->second.pauseTime,
|
|
|
|
i->second.pauseDelay);
|
|
|
|
const auto frameIndexNow = countFrameIndex(
|
|
|
|
state,
|
|
|
|
_lastSyncTime,
|
|
|
|
_delay);
|
|
|
|
PROFILE_LOG(("UNPAUSED WITH %1 DELAY AND %2 SKIPPED FRAMES").arg(_delay - i->second.pauseDelay).arg(frameIndexNow - frameIndexAtPaused));
|
|
|
|
state->addTimelineDelay(
|
|
|
|
(_delay - i->second.pauseDelay),
|
|
|
|
frameIndexNow - frameIndexAtPaused);
|
|
|
|
_active.emplace(animation, state);
|
|
|
|
_paused.erase(i);
|
|
|
|
}
|
|
|
|
|
2019-06-28 13:24:33 +00:00
|
|
|
void MultiPlayer::failed(not_null<Animation*> animation, Error error) {
|
|
|
|
//_updates.fire({ animation, error });
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<MultiUpdate> MultiPlayer::updates() const {
|
|
|
|
return _updates.events();
|
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::checkStep() {
|
2019-06-30 13:19:57 +00:00
|
|
|
if (_active.empty() || _nextFrameTime == kFrameDisplayTimeAlreadyDone) {
|
2019-06-29 12:24:46 +00:00
|
|
|
return;
|
|
|
|
} else if (_nextFrameTime != kTimeUnknown) {
|
2019-06-28 13:24:33 +00:00
|
|
|
checkNextFrameRender();
|
|
|
|
} else {
|
|
|
|
checkNextFrameAvailability();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::checkNextFrameAvailability() {
|
2019-06-29 12:24:46 +00:00
|
|
|
Expects(_nextFrameTime == kTimeUnknown);
|
|
|
|
|
2019-06-28 13:24:33 +00:00
|
|
|
auto next = kTimeUnknown;
|
|
|
|
for (const auto &[animation, state] : _active) {
|
|
|
|
const auto time = state->nextFrameDisplayTime();
|
|
|
|
if (time == kTimeUnknown) {
|
2019-06-29 12:24:46 +00:00
|
|
|
for (const auto &[animation, state] : _active) {
|
|
|
|
if (state->nextFrameDisplayTime() != kTimeUnknown) {
|
|
|
|
PROFILE_LOG(("PLAYER -------- SOME READY, BUT NOT ALL"));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2019-06-28 13:24:33 +00:00
|
|
|
return;
|
2019-06-29 12:24:46 +00:00
|
|
|
} else if (time == kFrameDisplayTimeAlreadyDone) {
|
|
|
|
continue;
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
|
|
|
if (next == kTimeUnknown || next > time) {
|
|
|
|
next = time;
|
|
|
|
}
|
|
|
|
}
|
2019-06-29 12:24:46 +00:00
|
|
|
if (next == kTimeUnknown) {
|
|
|
|
PROFILE_LOG(("PLAYER ALL DISPLAYED, WAITING PAINT."));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
PROFILE_LOG(("PLAYER NEXT FRAME TIME: %1").arg(next));
|
2019-06-28 13:24:33 +00:00
|
|
|
_nextFrameTime = next;
|
|
|
|
checkNextFrameRender();
|
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::checkNextFrameRender() {
|
|
|
|
Expects(_nextFrameTime != kTimeUnknown);
|
|
|
|
|
|
|
|
const auto now = crl::now();
|
|
|
|
if (now < _nextFrameTime) {
|
|
|
|
if (!_timer.isActive()) {
|
2019-06-29 12:24:46 +00:00
|
|
|
PROFILE_LOG(("PLAYER TIMER FOR: %1").arg(_nextFrameTime - now));
|
2019-06-28 13:24:33 +00:00
|
|
|
_timer.callOnce(_nextFrameTime - now);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_timer.cancel();
|
|
|
|
|
2019-06-30 13:19:57 +00:00
|
|
|
markFrameDisplayed(now);
|
|
|
|
addTimelineDelay(now - _nextFrameTime);
|
2019-07-01 11:20:53 +00:00
|
|
|
_lastSyncTime = now;
|
2019-06-30 13:19:57 +00:00
|
|
|
_nextFrameTime = kFrameDisplayTimeAlreadyDone;
|
2019-07-01 11:20:53 +00:00
|
|
|
processPending();
|
2019-06-28 13:24:33 +00:00
|
|
|
_updates.fire({});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::updateFrameRequest(
|
|
|
|
not_null<const Animation*> animation,
|
|
|
|
const FrameRequest &request) {
|
2019-07-01 11:20:53 +00:00
|
|
|
const auto state = [&] {
|
|
|
|
const auto key = animation;
|
|
|
|
if (const auto i = _active.find(animation); i != end(_active)) {
|
|
|
|
return i->second.get();
|
|
|
|
} else if (const auto j = _paused.find(animation);
|
|
|
|
j != end(_paused)) {
|
|
|
|
return j->second.state.get();
|
|
|
|
} else if (const auto k = _pendingToStart.find(animation);
|
|
|
|
k != end(_pendingToStart)) {
|
|
|
|
return k->second.state.get();
|
|
|
|
}
|
|
|
|
Unexpected("Animation in MultiPlayer::updateFrameRequest.");
|
|
|
|
}();
|
|
|
|
_renderer->updateFrameRequest(state, request);
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
|
|
|
|
2019-06-30 13:19:57 +00:00
|
|
|
void MultiPlayer::markFrameDisplayed(crl::time now) {
|
2019-06-28 13:24:33 +00:00
|
|
|
Expects(!_active.empty());
|
|
|
|
|
2019-06-29 12:24:46 +00:00
|
|
|
auto displayed = 0;
|
|
|
|
auto waiting = 0;
|
2019-06-28 13:24:33 +00:00
|
|
|
for (const auto &[animation, state] : _active) {
|
|
|
|
const auto time = state->nextFrameDisplayTime();
|
|
|
|
Assert(time != kTimeUnknown);
|
2019-06-29 12:24:46 +00:00
|
|
|
if (time == kFrameDisplayTimeAlreadyDone) {
|
|
|
|
continue;
|
|
|
|
} else if (now >= time) {
|
|
|
|
++displayed;
|
2019-06-30 13:19:57 +00:00
|
|
|
state->markFrameDisplayed(now);
|
2019-06-29 12:24:46 +00:00
|
|
|
} else {
|
|
|
|
++waiting;
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
|
|
|
}
|
2019-06-30 13:19:57 +00:00
|
|
|
PROFILE_LOG(("PLAYER FRAME DISPLAYED AT: %1 (MARKED %2, WAITING %3)").arg(now).arg(displayed).arg(waiting));
|
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::addTimelineDelay(crl::time delayed) {
|
|
|
|
Expects(!_active.empty());
|
|
|
|
|
|
|
|
for (const auto &[animation, state] : _active) {
|
|
|
|
state->addTimelineDelay(delayed);
|
|
|
|
}
|
|
|
|
_delay += delayed;
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void MultiPlayer::markFrameShown() {
|
2019-06-29 12:24:46 +00:00
|
|
|
if (_nextFrameTime == kFrameDisplayTimeAlreadyDone) {
|
|
|
|
_nextFrameTime = kTimeUnknown;
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
2019-06-29 12:24:46 +00:00
|
|
|
auto count = 0;
|
2019-06-28 13:24:33 +00:00
|
|
|
for (const auto &[animation, state] : _active) {
|
2019-07-01 11:20:53 +00:00
|
|
|
if (state->markFrameShown()) {
|
2019-06-29 12:24:46 +00:00
|
|
|
++count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
PROFILE_LOG(("PLAYER MARKED SHOWN %1 OF %2").arg(count).arg(_active.size()));
|
|
|
|
if (count) {
|
|
|
|
_renderer->frameShown();
|
2019-06-28 13:24:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace Lottie
|