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_player.h
media/streaming/media_streaming_reader.cpp media/streaming/media_streaming_reader.cpp
media/streaming/media_streaming_reader.h 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.cpp
media/streaming/media_streaming_utility.h media/streaming/media_streaming_utility.h
media/streaming/media_streaming_video_track.cpp 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/audio/media_audio_capture.h"
#include "media/player/media_player_button.h" #include "media/player/media_player_button.h"
#include "media/player/media_player_instance.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/round_video_recorder.h"
#include "ui/controls/send_button.h" #include "ui/controls/send_button.h"
#include "ui/effects/animation_value.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/effects/ripple_animation.h"
#include "ui/text/format_values.h" #include "ui/text/format_values.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/dynamic_image.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/widgets/tooltip.h" #include "ui/widgets/tooltip.h"
#include "ui/rect.h" #include "ui/rect.h"
@ -71,6 +74,61 @@ enum class FilterType {
Cancel, 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) { [[nodiscard]] auto InactiveColor(const QColor &c) {
return QColor(c.red(), c.green(), c.blue(), kInactiveWaveformBarAlpha); return QColor(c.red(), c.green(), c.blue(), kInactiveWaveformBarAlpha);
} }
@ -470,24 +528,26 @@ public:
const style::font &font); const style::font &font);
void requestPaintProgress(float64 progress); void requestPaintProgress(float64 progress);
rpl::producer<> stopRequests() const; [[nodiscard]] rpl::producer<> stopRequests() const;
void playPause(); void playPause();
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> videoPreview();
rpl::lifetime &lifetime(); [[nodiscard]] rpl::lifetime &lifetime();
private: private:
void init(); void init();
void initPlayButton(); void initPlayButton();
void initPlayProgress(); void initPlayProgress();
bool isInPlayer(const ::Media::Player::TrackState &state) const; [[nodiscard]] bool isInPlayer(
bool isInPlayer() const; const ::Media::Player::TrackState &state) const;
[[nodiscard]] bool isInPlayer() const;
int computeTopMargin(int height) const; [[nodiscard]] int computeTopMargin(int height) const;
QRect computeWaveformRect(const QRect &centerRect) const; [[nodiscard]] QRect computeWaveformRect(const QRect &centerRect) const;
not_null<Ui::RpWidget*> _parent; const not_null<Ui::RpWidget*> _parent;
const style::RecordBar &_st; const style::RecordBar &_st;
const not_null<Main::Session*> _session; const not_null<Main::Session*> _session;
@ -515,6 +575,7 @@ private:
anim::value _playProgress; anim::value _playProgress;
rpl::variable<float64> _showProgress = 0.; rpl::variable<float64> _showProgress = 0.;
rpl::event_stream<> _videoRepaints;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;
@ -716,6 +777,9 @@ void ListenWrap::initPlayButton() {
) | rpl::start_with_next([=](const State &state) { ) | rpl::start_with_next([=](const State &state) {
if (isInPlayer(state)) { if (isInPlayer(state)) {
*showPause = ShowPauseIcon(state.state); *showPause = ShowPauseIcon(state.state);
if (!_data->minithumbs.isNull()) {
_videoRepaints.fire({});
}
} else if (showPause->current()) { } else if (showPause->current()) {
*showPause = false; *showPause = false;
} }
@ -865,6 +929,12 @@ rpl::producer<> ListenWrap::stopRequests() const {
return _delete->clicks() | rpl::to_empty; 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() { rpl::lifetime &ListenWrap::lifetime() {
return _lifetime; return _lifetime;
} }
@ -1634,11 +1704,6 @@ void VoiceRecordBar::activeAnimate(bool active) {
} }
void VoiceRecordBar::visibilityAnimate(bool show, Fn<void()> &&callback) { 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) { if (_send->type() == Ui::SendButton::Type::Round) {
_level->setType(VoiceRecordButton::Type::Round); _level->setType(VoiceRecordButton::Type::Round);
} else { } else {
@ -1871,24 +1936,29 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
_cancelRequests.fire({}); _cancelRequests.fire({});
})); }));
} else if (type == StopType::Listen) { } else if (type == StopType::Listen) {
if (_videoRecorder) { if (const auto recorder = _videoRecorder.get()) {
const auto weak = Ui::MakeWeak(this); const auto weak = base::make_weak(recorder);
_videoRecorder->pause([=](Ui::RoundVideoResult data) { recorder->pause([=](Ui::RoundVideoResult data) {
crl::on_main([=, data = std::move(data)]() mutable { crl::on_main(weak, [=, data = std::move(data)]() mutable {
if (weak) { window()->raise();
window()->raise(); window()->activateWindow();
window()->activateWindow();
_paused = true; _paused = true;
_data = std::move(data); _data = std::move(data);
_listen = std::make_unique<ListenWrap>( _listen = std::make_unique<ListenWrap>(
this, this,
_st, _st,
&_show->session(), &_show->session(),
&_data, &_data,
_cancelFont); _cancelFont);
_listenChanges.fire({}); _listenChanges.fire({});
}
using SilentPreview = ::Media::Streaming::RoundPreview;
recorder->showPreview(
std::make_shared<SilentPreview>(
_data.content,
recorder->previewSize()),
_listen->videoPreview());
}); });
}); });
instance()->pause(true); instance()->pause(true);
@ -1933,11 +2003,11 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
: 0), : 0),
}; };
_sendVoiceRequests.fire({ _sendVoiceRequests.fire({
data.content, .bytes = data.content,
VoiceWaveform{}, //.waveform = {},
data.duration, .duration = data.duration,
options, .options = options,
true, .video = true,
}); });
} }
}); });
@ -1963,10 +2033,10 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
: 0), : 0),
}; };
_sendVoiceRequests.fire({ _sendVoiceRequests.fire({
_data.content, .bytes = _data.content,
_data.waveform, .waveform = _data.waveform,
_data.duration, .duration = _data.duration,
options, .options = options,
}); });
})); }));
} }
@ -2030,10 +2100,11 @@ void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {
options.ttlSeconds = std::numeric_limits<int>::max(); options.ttlSeconds = std::numeric_limits<int>::max();
} }
_sendVoiceRequests.fire({ _sendVoiceRequests.fire({
_data.content, .bytes = _data.content,
_data.waveform, .waveform = _data.waveform,
_data.duration, .duration = _data.duration,
options, .options = options,
.video = !_data.minithumbs.isNull(),
}); });
} }
} }

View file

@ -1200,6 +1200,21 @@ Streaming::Instance *Instance::roundVideoStreamed(HistoryItem *item) const {
return nullptr; 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( View::PlaybackProgress *Instance::roundVideoPlayback(
HistoryItem *item) const { HistoryItem *item) const {
return roundVideoStreamed(item) return roundVideoStreamed(item)

View file

@ -109,6 +109,9 @@ public:
[[nodiscard]] View::PlaybackProgress *roundVideoPlayback( [[nodiscard]] View::PlaybackProgress *roundVideoPlayback(
HistoryItem *item) const; HistoryItem *item) const;
[[nodiscard]] Streaming::Instance *roundVideoPreview(
not_null<DocumentData*> document) const;
[[nodiscard]] AudioMsgId current(AudioMsgId::Type type) const { [[nodiscard]] AudioMsgId current(AudioMsgId::Type type) const {
if (const auto data = getData(type)) { if (const auto data = getData(type)) {
return data->current; 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 "media/audio/media_audio_capture.h"
#include "ui/image/image_prepare.h" #include "ui/image/image_prepare.h"
#include "ui/arc_angles.h" #include "ui/arc_angles.h"
#include "ui/dynamic_image.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
#include "webrtc/webrtc_video_track.h" #include "webrtc/webrtc_video_track.h"
@ -33,6 +34,7 @@ constexpr auto kInitTimeout = 5 * crl::time(1000);
constexpr auto kBlurredSize = 64; constexpr auto kBlurredSize = 64;
constexpr auto kMinithumbsPerSecond = 5; constexpr auto kMinithumbsPerSecond = 5;
constexpr auto kMinithumbsInRow = 16; constexpr auto kMinithumbsInRow = 16;
constexpr auto kFadeDuration = crl::time(150);
using namespace FFmpeg; 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> { auto RoundVideoRecorder::updated() -> rpl::producer<Update, Error> {
return _private.producer_on_main([](const Private &that) { return _private.producer_on_main([](const Private &that) {
return that.updated(); return that.updated();
@ -1078,7 +1084,7 @@ void RoundVideoRecorder::progressTo(float64 progress) {
[=] { _preview->update(); }, [=] { _preview->update(); },
0., 0.,
1., 1.,
crl::time(200)); kFadeDuration);
} }
_progress = progress; _progress = progress;
_preview->update(); _preview->update();
@ -1225,6 +1231,30 @@ void RoundVideoRecorder::setup() {
(full / 4) - length, (full / 4) - length,
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()); }, raw->lifetime());
_descriptor.track->renderNextFrame() | rpl::start_with_next([=] { _descriptor.track->renderNextFrame() | rpl::start_with_next([=] {
@ -1257,7 +1287,24 @@ void RoundVideoRecorder::fade(bool visible) {
[=] { _preview->update(); }, [=] { _preview->update(); },
visible ? 0. : 1., visible ? 0. : 1.,
visible ? 1. : 0., 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) { void RoundVideoRecorder::pause(Fn<void(RoundVideoResult)> done) {
@ -1271,15 +1318,22 @@ void RoundVideoRecorder::pause(Fn<void(RoundVideoResult)> done) {
_paused = true; _paused = true;
prepareFrame(true); prepareFrame(true);
_progressReceived = false; _progressReceived = false;
_fadeContentAnimation.start( _fadeContentAnimation.start(updater(), 1., 0., kFadeDuration);
[=] { _preview->update(); },
1.,
0.,
crl::time(200));
_descriptor.track->setState(Webrtc::VideoState::Inactive); _descriptor.track->setState(Webrtc::VideoState::Inactive);
_preview->update(); _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) { void RoundVideoRecorder::resume(RoundVideoPartial partial) {
if (!_paused) { if (!_paused) {
return; return;
@ -1288,6 +1342,16 @@ void RoundVideoRecorder::resume(RoundVideoPartial partial) {
that.restart(std::move(partial)); that.restart(std::move(partial));
}); });
_paused = false; _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); _descriptor.track->setState(Webrtc::VideoState::Active);
_preview->update(); _preview->update();
} }

View file

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