Add recorded round video preview.

This commit is contained in:
John Preston 2024-10-22 15:01:53 +04:00
parent 9514b6eecd
commit 6cfa053328
8 changed files with 320 additions and 49 deletions

View file

@ -1162,6 +1162,8 @@ PRIVATE
media/streaming/media_streaming_player.h
media/streaming/media_streaming_reader.cpp
media/streaming/media_streaming_reader.h
media/streaming/media_streaming_round_preview.cpp
media/streaming/media_streaming_round_preview.h
media/streaming/media_streaming_utility.cpp
media/streaming/media_streaming_utility.h
media/streaming/media_streaming_video_track.cpp

View file

@ -28,6 +28,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/audio/media_audio_capture.h"
#include "media/player/media_player_button.h"
#include "media/player/media_player_instance.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_round_preview.h"
#include "ui/controls/round_video_recorder.h"
#include "ui/controls/send_button.h"
#include "ui/effects/animation_value.h"
@ -35,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/ripple_animation.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/dynamic_image.h"
#include "ui/painter.h"
#include "ui/widgets/tooltip.h"
#include "ui/rect.h"
@ -71,6 +74,61 @@ enum class FilterType {
Cancel,
};
class SoundedPreview final : public Ui::DynamicImage {
public:
SoundedPreview(
not_null<DocumentData*> document,
rpl::producer<> repaints);
std::shared_ptr<DynamicImage> clone() override;
QImage image(int size) override;
void subscribeToUpdates(Fn<void()> callback) override;
private:
const not_null<DocumentData*> _document;
QImage _roundingMask;
Fn<void()> _repaint;
rpl::lifetime _lifetime;
};
SoundedPreview::SoundedPreview(
not_null<DocumentData*> document,
rpl::producer<> repaints)
: _document(document) {
std::move(repaints) | rpl::start_with_next([=] {
if (const auto onstack = _repaint) {
onstack();
}
}, _lifetime);
}
std::shared_ptr<Ui::DynamicImage> SoundedPreview::clone() {
Unexpected("ListenWrap::videoPreview::clone.");
}
QImage SoundedPreview::image(int size) {
const auto player = ::Media::Player::instance();
const auto streamed = player->roundVideoPreview(_document);
if (!streamed) {
return {};
}
const auto full = QSize(size, size) * style::DevicePixelRatio();
if (_roundingMask.size() != full) {
_roundingMask = Images::EllipseMask(full);
}
const auto frame = streamed->frameWithInfo({
.resize = full,
.outer = full,
.mask = _roundingMask,
});
return frame.image;
}
void SoundedPreview::subscribeToUpdates(Fn<void()> callback) {
_repaint = std::move(callback);
}
[[nodiscard]] auto InactiveColor(const QColor &c) {
return QColor(c.red(), c.green(), c.blue(), kInactiveWaveformBarAlpha);
}
@ -470,24 +528,26 @@ public:
const style::font &font);
void requestPaintProgress(float64 progress);
rpl::producer<> stopRequests() const;
[[nodiscard]] rpl::producer<> stopRequests() const;
void playPause();
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> videoPreview();
rpl::lifetime &lifetime();
[[nodiscard]] rpl::lifetime &lifetime();
private:
void init();
void initPlayButton();
void initPlayProgress();
bool isInPlayer(const ::Media::Player::TrackState &state) const;
bool isInPlayer() const;
[[nodiscard]] bool isInPlayer(
const ::Media::Player::TrackState &state) const;
[[nodiscard]] bool isInPlayer() const;
int computeTopMargin(int height) const;
QRect computeWaveformRect(const QRect &centerRect) const;
[[nodiscard]] int computeTopMargin(int height) const;
[[nodiscard]] QRect computeWaveformRect(const QRect &centerRect) const;
not_null<Ui::RpWidget*> _parent;
const not_null<Ui::RpWidget*> _parent;
const style::RecordBar &_st;
const not_null<Main::Session*> _session;
@ -515,6 +575,7 @@ private:
anim::value _playProgress;
rpl::variable<float64> _showProgress = 0.;
rpl::event_stream<> _videoRepaints;
rpl::lifetime _lifetime;
@ -716,6 +777,9 @@ void ListenWrap::initPlayButton() {
) | rpl::start_with_next([=](const State &state) {
if (isInPlayer(state)) {
*showPause = ShowPauseIcon(state.state);
if (!_data->minithumbs.isNull()) {
_videoRepaints.fire({});
}
} else if (showPause->current()) {
*showPause = false;
}
@ -865,6 +929,12 @@ rpl::producer<> ListenWrap::stopRequests() const {
return _delete->clicks() | rpl::to_empty;
}
std::shared_ptr<Ui::DynamicImage> ListenWrap::videoPreview() {
return std::make_shared<SoundedPreview>(
_document,
_videoRepaints.events());
}
rpl::lifetime &ListenWrap::lifetime() {
return _lifetime;
}
@ -1634,11 +1704,6 @@ void VoiceRecordBar::activeAnimate(bool active) {
}
void VoiceRecordBar::visibilityAnimate(bool show, Fn<void()> &&callback) {
//if (_videoRecorder) {
// _videoHiding.push_back(base::take(_videoRecorder));
// _videoHiding.back()->hide();
//}
AssertIsDebug();
if (_send->type() == Ui::SendButton::Type::Round) {
_level->setType(VoiceRecordButton::Type::Round);
} else {
@ -1871,24 +1936,29 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
_cancelRequests.fire({});
}));
} else if (type == StopType::Listen) {
if (_videoRecorder) {
const auto weak = Ui::MakeWeak(this);
_videoRecorder->pause([=](Ui::RoundVideoResult data) {
crl::on_main([=, data = std::move(data)]() mutable {
if (weak) {
window()->raise();
window()->activateWindow();
if (const auto recorder = _videoRecorder.get()) {
const auto weak = base::make_weak(recorder);
recorder->pause([=](Ui::RoundVideoResult data) {
crl::on_main(weak, [=, data = std::move(data)]() mutable {
window()->raise();
window()->activateWindow();
_paused = true;
_data = std::move(data);
_listen = std::make_unique<ListenWrap>(
this,
_st,
&_show->session(),
&_data,
_cancelFont);
_listenChanges.fire({});
}
_paused = true;
_data = std::move(data);
_listen = std::make_unique<ListenWrap>(
this,
_st,
&_show->session(),
&_data,
_cancelFont);
_listenChanges.fire({});
using SilentPreview = ::Media::Streaming::RoundPreview;
recorder->showPreview(
std::make_shared<SilentPreview>(
_data.content,
recorder->previewSize()),
_listen->videoPreview());
});
});
instance()->pause(true);
@ -1933,11 +2003,11 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
: 0),
};
_sendVoiceRequests.fire({
data.content,
VoiceWaveform{},
data.duration,
options,
true,
.bytes = data.content,
//.waveform = {},
.duration = data.duration,
.options = options,
.video = true,
});
}
});
@ -1963,10 +2033,10 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
: 0),
};
_sendVoiceRequests.fire({
_data.content,
_data.waveform,
_data.duration,
options,
.bytes = _data.content,
.waveform = _data.waveform,
.duration = _data.duration,
.options = options,
});
}));
}
@ -2030,10 +2100,11 @@ void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {
options.ttlSeconds = std::numeric_limits<int>::max();
}
_sendVoiceRequests.fire({
_data.content,
_data.waveform,
_data.duration,
options,
.bytes = _data.content,
.waveform = _data.waveform,
.duration = _data.duration,
.options = options,
.video = !_data.minithumbs.isNull(),
});
}
}

View file

@ -1200,6 +1200,21 @@ Streaming::Instance *Instance::roundVideoStreamed(HistoryItem *item) const {
return nullptr;
}
Streaming::Instance *Instance::roundVideoPreview(
not_null<DocumentData*> document) const {
if (const auto data = getData(AudioMsgId::Type::Voice)) {
if (const auto streamed = data->streamed.get()) {
if (streamed->id.audio() == document) {
const auto player = &streamed->instance.player();
if (player->ready() && !player->videoSize().isEmpty()) {
return &streamed->instance;
}
}
}
}
return nullptr;
}
View::PlaybackProgress *Instance::roundVideoPlayback(
HistoryItem *item) const {
return roundVideoStreamed(item)

View file

@ -109,6 +109,9 @@ public:
[[nodiscard]] View::PlaybackProgress *roundVideoPlayback(
HistoryItem *item) const;
[[nodiscard]] Streaming::Instance *roundVideoPreview(
not_null<DocumentData*> document) const;
[[nodiscard]] AudioMsgId current(AudioMsgId::Type type) const {
if (const auto data = getData(type)) {
return data->current;

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
*/
#include "media/streaming/media_streaming_round_preview.h"
namespace Media::Streaming {
RoundPreview::RoundPreview(const QByteArray &bytes, int size)
: _bytes(bytes)
, _reader(
Clip::MakeReader(_bytes, [=](Clip::Notification update) {
clipCallback(update);
}))
, _size(size) {
}
std::shared_ptr<Ui::DynamicImage> RoundPreview::clone() {
Unexpected("RoundPreview::clone.");
}
QImage RoundPreview::image(int size) {
if (!_reader || !_reader->started()) {
return QImage();
}
return _reader->current({
.frame = QSize(_size, _size),
.factor = style::DevicePixelRatio(),
.radius = ImageRoundRadius::Ellipse,
}, crl::now());
}
void RoundPreview::subscribeToUpdates(Fn<void()> callback) {
_repaint = std::move(callback);
}
void RoundPreview::clipCallback(Clip::Notification notification) {
switch (notification) {
case Clip::Notification::Reinit: {
if (_reader->state() == ::Media::Clip::State::Error) {
_reader.setBad();
} else if (_reader->ready() && !_reader->started()) {
_reader->start({
.frame = QSize(_size, _size),
.factor = style::DevicePixelRatio(),
.radius = ImageRoundRadius::Ellipse,
});
}
} break;
case Clip::Notification::Repaint: break;
}
if (const auto onstack = _repaint) {
onstack();
}
}
} // namespace Media::Streaming

View file

@ -0,0 +1,35 @@
/*
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/dynamic_image.h"
#include "media/clip/media_clip_reader.h"
namespace Media::Streaming {
class RoundPreview final : public Ui::DynamicImage {
public:
RoundPreview(const QByteArray &bytes, int size);
std::shared_ptr<DynamicImage> clone() override;
QImage image(int size) override;
void subscribeToUpdates(Fn<void()> callback) override;
private:
void clipCallback(Clip::Notification notification);
const QByteArray _bytes;
Clip::ReaderPointer _reader;
Fn<void()> _repaint;
int _size = 0;
};
} // namespace Media::Streaming

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/audio/media_audio_capture.h"
#include "ui/image/image_prepare.h"
#include "ui/arc_angles.h"
#include "ui/dynamic_image.h"
#include "ui/painter.h"
#include "ui/rp_widget.h"
#include "webrtc/webrtc_video_track.h"
@ -33,6 +34,7 @@ constexpr auto kInitTimeout = 5 * crl::time(1000);
constexpr auto kBlurredSize = 64;
constexpr auto kMinithumbsPerSecond = 5;
constexpr auto kMinithumbsInRow = 16;
constexpr auto kFadeDuration = crl::time(150);
using namespace FFmpeg;
@ -1044,6 +1046,10 @@ Fn<void(Media::Capture::Chunk)> RoundVideoRecorder::audioChunkProcessor() {
};
}
int RoundVideoRecorder::previewSize() const {
return _side;
}
auto RoundVideoRecorder::updated() -> rpl::producer<Update, Error> {
return _private.producer_on_main([](const Private &that) {
return that.updated();
@ -1078,7 +1084,7 @@ void RoundVideoRecorder::progressTo(float64 progress) {
[=] { _preview->update(); },
0.,
1.,
crl::time(200));
kFadeDuration);
}
_progress = progress;
_preview->update();
@ -1225,6 +1231,30 @@ void RoundVideoRecorder::setup() {
(full / 4) - length,
length);
}
const auto preview = _fadePreviewAnimation.value(
_silentPreview ? 1. : 0.);
const auto frame = _silentPreview
? lookupPreviewFrame()
: _cachedPreviewFrame;
if (preview > 0. && !frame.image.isNull()) {
p.setOpacity(preview);
p.drawImage(inner, frame.image);
if (frame.silent) {
const auto iconSize = st::historyVideoMessageMuteSize;
const auto iconRect = style::rtlrect(
inner.x() + (inner.width() - iconSize) / 2,
inner.y() + st::msgDateImgDelta,
iconSize,
iconSize,
raw->width());
p.setPen(Qt::NoPen);
p.setBrush(st::msgDateImgBg);
auto hq = PainterHighQualityEnabler(p);
p.drawEllipse(iconRect);
st::historyVideoMessageMute.paintInCenter(p, iconRect);
}
}
}, raw->lifetime());
_descriptor.track->renderNextFrame() | rpl::start_with_next([=] {
@ -1257,7 +1287,24 @@ void RoundVideoRecorder::fade(bool visible) {
[=] { _preview->update(); },
visible ? 0. : 1.,
visible ? 1. : 0.,
crl::time(200));
kFadeDuration);
}
auto RoundVideoRecorder::lookupPreviewFrame() const -> PreviewFrame {
auto sounded = _soundedPreview
? _soundedPreview->image(_side)
: QImage();
const auto silent = (_silentPreview && sounded.isNull());
return {
.image = silent ? _silentPreview->image(_side) : std::move(sounded),
.silent = silent,
};
}
Fn<void()> RoundVideoRecorder::updater() const {
return [=] {
_preview->update();
};
}
void RoundVideoRecorder::pause(Fn<void(RoundVideoResult)> done) {
@ -1271,15 +1318,22 @@ void RoundVideoRecorder::pause(Fn<void(RoundVideoResult)> done) {
_paused = true;
prepareFrame(true);
_progressReceived = false;
_fadeContentAnimation.start(
[=] { _preview->update(); },
1.,
0.,
crl::time(200));
_fadeContentAnimation.start(updater(), 1., 0., kFadeDuration);
_descriptor.track->setState(Webrtc::VideoState::Inactive);
_preview->update();
}
void RoundVideoRecorder::showPreview(
std::shared_ptr<Ui::DynamicImage> silent,
std::shared_ptr<Ui::DynamicImage> sounded) {
_silentPreview = std::move(silent);
_soundedPreview = std::move(sounded);
_silentPreview->subscribeToUpdates(updater());
_soundedPreview->subscribeToUpdates(updater());
_fadePreviewAnimation.start(updater(), 0., 1., kFadeDuration);
_preview->update();
}
void RoundVideoRecorder::resume(RoundVideoPartial partial) {
if (!_paused) {
return;
@ -1288,6 +1342,16 @@ void RoundVideoRecorder::resume(RoundVideoPartial partial) {
that.restart(std::move(partial));
});
_paused = false;
_cachedPreviewFrame = lookupPreviewFrame();
if (const auto preview = base::take(_silentPreview)) {
preview->subscribeToUpdates(nullptr);
}
if (const auto preview = base::take(_soundedPreview)) {
preview->subscribeToUpdates(nullptr);
}
if (!_cachedPreviewFrame.image.isNull()) {
_fadePreviewAnimation.start(updater(), 1., 0., kFadeDuration);
}
_descriptor.track->setState(Webrtc::VideoState::Active);
_preview->update();
}

View file

@ -29,6 +29,7 @@ class VideoTrack;
namespace Ui {
class RpWidget;
class DynamicImage;
class RoundVideoRecorder;
struct RoundVideoRecorderDescriptor {
@ -58,18 +59,27 @@ public:
explicit RoundVideoRecorder(RoundVideoRecorderDescriptor &&descriptor);
~RoundVideoRecorder();
[[nodiscard]] int previewSize() const;
[[nodiscard]] Fn<void(Media::Capture::Chunk)> audioChunkProcessor();
void pause(Fn<void(RoundVideoResult)> done = nullptr);
void resume(RoundVideoPartial partial);
void hide(Fn<void(RoundVideoResult)> done = nullptr);
void showPreview(
std::shared_ptr<Ui::DynamicImage> silent,
std::shared_ptr<Ui::DynamicImage> sounded);
using Update = Media::Capture::Update;
using Error = Media::Capture::Error;
[[nodiscard]] rpl::producer<Update, Error> updated();
private:
class Private;
struct PreviewFrame {
QImage image;
bool silent = false;
};
void setup();
void prepareFrame(bool blurred = false);
@ -77,12 +87,21 @@ private:
void progressTo(float64 progress);
void fade(bool visible);
[[nodiscard]] Fn<void()> updater() const;
[[nodiscard]] PreviewFrame lookupPreviewFrame() const;
const RoundVideoRecorderDescriptor _descriptor;
std::unique_ptr<RpWidget> _preview;
crl::object_on_queue<Private> _private;
Ui::Animations::Simple _progressAnimation;
Ui::Animations::Simple _fadeAnimation;
Ui::Animations::Simple _fadeContentAnimation;
std::shared_ptr<Ui::DynamicImage> _silentPreview;
std::shared_ptr<Ui::DynamicImage> _soundedPreview;
Ui::Animations::Simple _fadePreviewAnimation;
PreviewFrame _cachedPreviewFrame;
float64 _progress = 0.;
QImage _frameOriginal;
QImage _framePlaceholder;