diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index f378d925a..1b640372d 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -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 diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index b24d7ddd8..754f1ab21 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -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 document, + rpl::producer<> repaints); + std::shared_ptr clone() override; + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + const not_null _document; + QImage _roundingMask; + Fn _repaint; + rpl::lifetime _lifetime; + +}; + +SoundedPreview::SoundedPreview( + not_null document, + rpl::producer<> repaints) +: _document(document) { + std::move(repaints) | rpl::start_with_next([=] { + if (const auto onstack = _repaint) { + onstack(); + } + }, _lifetime); +} + +std::shared_ptr 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 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 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 ¢erRect) const; + [[nodiscard]] int computeTopMargin(int height) const; + [[nodiscard]] QRect computeWaveformRect(const QRect ¢erRect) const; - not_null _parent; + const not_null _parent; const style::RecordBar &_st; const not_null _session; @@ -515,6 +575,7 @@ private: anim::value _playProgress; rpl::variable _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 ListenWrap::videoPreview() { + return std::make_shared( + _document, + _videoRepaints.events()); +} + rpl::lifetime &ListenWrap::lifetime() { return _lifetime; } @@ -1634,11 +1704,6 @@ void VoiceRecordBar::activeAnimate(bool active) { } void VoiceRecordBar::visibilityAnimate(bool show, Fn &&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( - this, - _st, - &_show->session(), - &_data, - _cancelFont); - _listenChanges.fire({}); - } + _paused = true; + _data = std::move(data); + _listen = std::make_unique( + this, + _st, + &_show->session(), + &_data, + _cancelFont); + _listenChanges.fire({}); + + using SilentPreview = ::Media::Streaming::RoundPreview; + recorder->showPreview( + std::make_shared( + _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::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(), }); } } diff --git a/Telegram/SourceFiles/media/player/media_player_instance.cpp b/Telegram/SourceFiles/media/player/media_player_instance.cpp index 4dc687a27..2859aa068 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.cpp +++ b/Telegram/SourceFiles/media/player/media_player_instance.cpp @@ -1200,6 +1200,21 @@ Streaming::Instance *Instance::roundVideoStreamed(HistoryItem *item) const { return nullptr; } +Streaming::Instance *Instance::roundVideoPreview( + not_null 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) diff --git a/Telegram/SourceFiles/media/player/media_player_instance.h b/Telegram/SourceFiles/media/player/media_player_instance.h index fce26855b..13771ce4c 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.h +++ b/Telegram/SourceFiles/media/player/media_player_instance.h @@ -109,6 +109,9 @@ public: [[nodiscard]] View::PlaybackProgress *roundVideoPlayback( HistoryItem *item) const; + [[nodiscard]] Streaming::Instance *roundVideoPreview( + not_null document) const; + [[nodiscard]] AudioMsgId current(AudioMsgId::Type type) const { if (const auto data = getData(type)) { return data->current; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.cpp new file mode 100644 index 000000000..56beb61ec --- /dev/null +++ b/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.cpp @@ -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 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 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 diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.h b/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.h new file mode 100644 index 000000000..64440bc49 --- /dev/null +++ b/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.h @@ -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 clone() override; + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + void clipCallback(Clip::Notification notification); + + const QByteArray _bytes; + Clip::ReaderPointer _reader; + Fn _repaint; + int _size = 0; + +}; + +} // namespace Media::Streaming diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp index e26e3af6c..7d9cf9689 100644 --- a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp @@ -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 RoundVideoRecorder::audioChunkProcessor() { }; } +int RoundVideoRecorder::previewSize() const { + return _side; +} + auto RoundVideoRecorder::updated() -> rpl::producer { 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 RoundVideoRecorder::updater() const { + return [=] { + _preview->update(); + }; } void RoundVideoRecorder::pause(Fn done) { @@ -1271,15 +1318,22 @@ void RoundVideoRecorder::pause(Fn 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 silent, + std::shared_ptr 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(); } diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.h b/Telegram/SourceFiles/ui/controls/round_video_recorder.h index 51f0e537d..ecef3e425 100644 --- a/Telegram/SourceFiles/ui/controls/round_video_recorder.h +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.h @@ -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 audioChunkProcessor(); void pause(Fn done = nullptr); void resume(RoundVideoPartial partial); void hide(Fn done = nullptr); + void showPreview( + std::shared_ptr silent, + std::shared_ptr sounded); + using Update = Media::Capture::Update; using Error = Media::Capture::Error; [[nodiscard]] rpl::producer 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 updater() const; + [[nodiscard]] PreviewFrame lookupPreviewFrame() const; + const RoundVideoRecorderDescriptor _descriptor; std::unique_ptr _preview; crl::object_on_queue _private; Ui::Animations::Simple _progressAnimation; Ui::Animations::Simple _fadeAnimation; Ui::Animations::Simple _fadeContentAnimation; + + std::shared_ptr _silentPreview; + std::shared_ptr _soundedPreview; + Ui::Animations::Simple _fadePreviewAnimation; + PreviewFrame _cachedPreviewFrame; + float64 _progress = 0.; QImage _frameOriginal; QImage _framePlaceholder;