From 552343fa37fdec7c0be9adcdf05376d02a034465 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 4 Oct 2024 14:38:49 +0400 Subject: [PATCH] PoC video messages sending. --- Telegram/SourceFiles/apiwrap.cpp | 2 + Telegram/SourceFiles/apiwrap.h | 1 + .../ui/desktop_capture_choose_source.cpp | 4 +- .../SourceFiles/ffmpeg/ffmpeg_utility.cpp | 173 ++++- Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h | 47 +- .../SourceFiles/history/history_widget.cpp | 1 + .../view/controls/compose_controls_common.h | 1 + .../history_view_voice_record_bar.cpp | 88 ++- .../controls/history_view_voice_record_bar.h | 11 +- .../view/history_view_replies_section.cpp | 1 + .../view/history_view_scheduled_section.cpp | 288 ++++---- .../view/history_view_scheduled_section.h | 13 +- .../media/audio/media_audio_capture.cpp | 75 +- .../media/audio/media_audio_capture.h | 11 +- .../media/stories/media_stories_reply.cpp | 1 + .../business/settings_shortcut_messages.cpp | 1 + .../SourceFiles/storage/localimageloader.cpp | 47 +- .../SourceFiles/storage/localimageloader.h | 2 + .../ui/controls/round_video_recorder.cpp | 649 ++++++++++++++++++ .../ui/controls/round_video_recorder.h | 65 ++ Telegram/cmake/td_ui.cmake | 4 + Telegram/lib_webrtc | 2 +- 22 files changed, 1278 insertions(+), 209 deletions(-) create mode 100644 Telegram/SourceFiles/ui/controls/round_video_recorder.cpp create mode 100644 Telegram/SourceFiles/ui/controls/round_video_recorder.h diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 5ef4af3d6..4f23323ab 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3502,6 +3502,7 @@ void ApiWrap::sendVoiceMessage( QByteArray result, VoiceWaveform waveform, crl::time duration, + bool video, const SendAction &action) { const auto caption = TextWithTags(); const auto to = FileLoadTaskOptions(action); @@ -3510,6 +3511,7 @@ void ApiWrap::sendVoiceMessage( result, duration, waveform, + video, to, caption)); } diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 7259c410d..18a505de9 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -317,6 +317,7 @@ public: QByteArray result, VoiceWaveform waveform, crl::time duration, + bool video, const SendAction &action); void sendFiles( Ui::PreparedList &&list, diff --git a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp index 4e77023f2..a122f09a2 100644 --- a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp +++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp @@ -73,8 +73,8 @@ private: SourceButton _widget; FlatLabel _label; - RoundRect _selectedRect; - RoundRect _activeRect; + Ui::RoundRect _selectedRect; + Ui::RoundRect _activeRect; tgcalls::DesktopCaptureSource _source; std::unique_ptr<Preview> _preview; rpl::event_stream<> _activations; diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index 0f7083df3..8fd0f3e97 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -284,10 +284,12 @@ FormatPointer MakeFormatPointer( return {}; } result->pb = io.get(); + result->flags |= AVFMT_FLAG_CUSTOM_IO; auto options = (AVDictionary*)nullptr; const auto guard = gsl::finally([&] { av_dict_free(&options); }); av_dict_set(&options, "usetoc", "1", 0); + const auto error = AvErrorWrap(avformat_open_input( &result, nullptr, @@ -307,6 +309,54 @@ FormatPointer MakeFormatPointer( return FormatPointer(result); } +FormatPointer MakeWriteFormatPointer( + void *opaque, + int(*read)(void *opaque, uint8_t *buffer, int bufferSize), +#if DA_FFMPEG_CONST_WRITE_CALLBACK + int(*write)(void *opaque, const uint8_t *buffer, int bufferSize), +#else + int(*write)(void *opaque, uint8_t *buffer, int bufferSize), +#endif + int64_t(*seek)(void *opaque, int64_t offset, int whence), + const QByteArray &format) { + const AVOutputFormat *found = nullptr; + void *i = nullptr; + while ((found = av_muxer_iterate(&i))) { + if (found->name == format) { + break; + } + } + if (!found) { + LogError( + "av_muxer_iterate", + u"Format %1 not found"_q.arg(QString::fromUtf8(format))); + return {}; + } + + auto io = MakeIOPointer(opaque, read, write, seek); + if (!io) { + return {}; + } + io->seekable = (seek != nullptr); + + auto result = (AVFormatContext*)nullptr; + auto error = AvErrorWrap(avformat_alloc_output_context2( + &result, + (AVOutputFormat*)found, + nullptr, + nullptr)); + if (!result || error) { + LogError("avformat_alloc_output_context2", error); + return {}; + } + result->pb = io.get(); + result->flags |= AVFMT_FLAG_CUSTOM_IO; + + // Now FormatPointer will own and free the IO context. + io.release(); + return FormatPointer(result); +} + void FormatDeleter::operator()(AVFormatContext *value) { if (value) { const auto deleter = IOPointer(value->pb); @@ -448,21 +498,134 @@ SwscalePointer MakeSwscalePointer( existing); } +void SwresampleDeleter::operator()(SwrContext *value) { + if (value) { + swr_free(&value); + } +} + +SwresamplePointer MakeSwresamplePointer( +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVChannelLayout *srcLayout, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + uint64_t srcLayout, +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVSampleFormat srcFormat, + int srcRate, +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVChannelLayout *dstLayout, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + uint64_t dstLayout, +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVSampleFormat dstFormat, + int dstRate, + SwresamplePointer *existing) { + // We have to use custom caching for SwsContext, because + // sws_getCachedContext checks passed flags with existing context flags, + // and re-creates context if they're different, but in the process of + // context creation the passed flags are modified before being written + // to the resulting context, so the caching doesn't work. + if (existing && (*existing) != nullptr) { + const auto &deleter = existing->get_deleter(); + if (true +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + && srcLayout->nb_channels == deleter.srcChannels + && dstLayout->nb_channels == deleter.dstChannels +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + && (av_get_channel_layout_nb_channels(srcLayout) + == deleter.srcChannels) + && (av_get_channel_layout_nb_channels(dstLayout) + == deleter.dstChannels) +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + && srcFormat == deleter.srcFormat + && dstFormat == deleter.dstFormat + && srcRate == deleter.srcRate + && dstRate == deleter.dstRate) { + return std::move(*existing); + } + } + + // Initialize audio resampler +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + auto result = (SwrContext*)nullptr; + auto error = AvErrorWrap(swr_alloc_set_opts2( + &result, + dstLayout, + dstFormat, + dstRate, + srcLayout, + srcFormat, + srcRate, + 0, + nullptr)); + if (error || !result) { + LogError(u"swr_alloc_set_opts2"_q, error); + return SwresamplePointer(); + } +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + auto result = swr_alloc_set_opts( + existing ? existing.get() : nullptr, + dstLayout, + dstFormat, + dstRate, + srcLayout, + srcFormat, + srcRate, + 0, + nullptr); + if (!result) { + LogError(u"swr_alloc_set_opts"_q); + } +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + + error = AvErrorWrap(swr_init(result)); + if (error) { + LogError(u"swr_init"_q, error); + swr_free(&result); + return SwresamplePointer(); + } + + return SwresamplePointer( + result, + { + srcFormat, + srcRate, +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + srcLayout->nb_channels, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + av_get_channel_layout_nb_channels(srcLayout), +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + dstFormat, + dstRate, +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + dstLayout->nb_channels, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + av_get_channel_layout_nb_channels(dstLayout), +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + }); +} + void SwscaleDeleter::operator()(SwsContext *value) { if (value) { sws_freeContext(value); } } -void LogError(const QString &method) { - LOG(("Streaming Error: Error in %1.").arg(method)); +void LogError(const QString &method, const QString &details) { + LOG(("Streaming Error: Error in %1%2." + ).arg(method + ).arg(details.isEmpty() ? QString() : " - " + details)); } -void LogError(const QString &method, AvErrorWrap error) { - LOG(("Streaming Error: Error in %1 (code: %2, text: %3)." +void LogError( + const QString &method, + AvErrorWrap error, + const QString &details) { + LOG(("Streaming Error: Error in %1 (code: %2, text: %3)%4." ).arg(method ).arg(error.code() - ).arg(error.text())); + ).arg(error.text() + ).arg(details.isEmpty() ? QString() : " - " + details)); } crl::time PtsToTime(int64_t pts, AVRational timeBase) { diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h index d96daa9c7..6397cbb92 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h @@ -19,6 +19,8 @@ extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> +#include <libswresample/swresample.h> +#include <libavutil/opt.h> #include <libavutil/version.h> } // extern "C" @@ -138,6 +140,16 @@ using FormatPointer = std::unique_ptr<AVFormatContext, FormatDeleter>; int(*write)(void *opaque, uint8_t *buffer, int bufferSize), #endif int64_t(*seek)(void *opaque, int64_t offset, int whence)); +[[nodiscard]] FormatPointer MakeWriteFormatPointer( + void *opaque, + int(*read)(void *opaque, uint8_t *buffer, int bufferSize), +#if DA_FFMPEG_CONST_WRITE_CALLBACK + int(*write)(void *opaque, const uint8_t *buffer, int bufferSize), +#else + int(*write)(void *opaque, uint8_t *buffer, int bufferSize), +#endif + int64_t(*seek)(void *opaque, int64_t offset, int whence), + const QByteArray &format); struct CodecDeleter { void operator()(AVCodecContext *value); @@ -179,8 +191,39 @@ using SwscalePointer = std::unique_ptr<SwsContext, SwscaleDeleter>; QSize resize, SwscalePointer *existing = nullptr); -void LogError(const QString &method); -void LogError(const QString &method, FFmpeg::AvErrorWrap error); +struct SwresampleDeleter { + AVSampleFormat srcFormat = AV_SAMPLE_FMT_NONE; + int srcRate = 0; + int srcChannels = 0; + AVSampleFormat dstFormat = AV_SAMPLE_FMT_NONE; + int dstRate = 0; + int dstChannels = 0; + + void operator()(SwrContext *value); +}; +using SwresamplePointer = std::unique_ptr<SwrContext, SwresampleDeleter>; +[[nodiscard]] SwresamplePointer MakeSwresamplePointer( +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVChannelLayout *srcLayout, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + uint64_t srcLayout, +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVSampleFormat srcFormat, + int srcRate, +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVChannelLayout *dstLayout, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + uint64_t dstLayout, +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVSampleFormat dstFormat, + int dstRate, + SwresamplePointer *existing = nullptr); + +void LogError(const QString &method, const QString &details = {}); +void LogError( + const QString &method, + FFmpeg::AvErrorWrap error, + const QString &details = {}); [[nodiscard]] const AVCodec *FindDecoder(not_null<AVCodecContext*> context); [[nodiscard]] crl::time PtsToTime(int64_t pts, AVRational timeBase); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 56c61b71b..7f71f6549 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1042,6 +1042,7 @@ void HistoryWidget::initVoiceRecordBar() { data.bytes, data.waveform, data.duration, + data.video, action); _voiceRecordBar->clearListenState(); }, lifetime()); diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index e51bae134..bebcc9c57 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -28,6 +28,7 @@ struct VoiceToSend { VoiceWaveform waveform; crl::time duration = 0; Api::SendOptions options; + bool video = false; }; struct SendActionUpdate { Api::SendProgressType type = Api::SendProgressType(); 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 837cc0957..7ed5bf0db 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 @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/random.h" #include "base/unixtime.h" #include "ui/boxes/confirm_box.h" +#include "calls/calls_instance.h" #include "chat_helpers/compose/compose_show.h" #include "core/application.h" #include "data/data_document.h" @@ -27,6 +28,7 @@ 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 "ui/controls/round_video_recorder.h" #include "ui/controls/send_button.h" #include "ui/effects/animation_value.h" #include "ui/effects/animation_value_f.h" @@ -37,11 +39,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/tooltip.h" #include "ui/rect.h" #include "ui/ui_utility.h" +#include "webrtc/webrtc_video_track.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_layers.h" #include "styles/style_media_player.h" +#include <tgcalls/VideoCaptureInterface.h> + namespace HistoryView::Controls { namespace { @@ -1579,6 +1584,11 @@ 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(); const auto to = show ? 1. : 0.; const auto from = show ? 0. : 1.; auto animationCallback = [=, callback = std::move(callback)](auto value) { @@ -1646,12 +1656,17 @@ void VoiceRecordBar::startRecording() { if (isRecording()) { return; } + _recordingVideo = true; AssertIsDebug(); auto appearanceCallback = [=] { if (_showAnimation.animating()) { return; } using namespace ::Media::Capture; + if (_recordingVideo && !createVideoRecorder()) { + stop(false); + return; + } if (!instance()->available()) { stop(false); return; @@ -1664,8 +1679,13 @@ void VoiceRecordBar::startRecording() { if (_paused.current()) { _paused = false; instance()->pause(false, nullptr); + if (_videoRecorder) { + _videoRecorder->setPaused(false); + } } else { - instance()->start(); + instance()->start(_videoRecorder + ? _videoRecorder->audioChunkProcessor() + : nullptr); } instance()->updated( ) | rpl::start_with_next_error([=](const Update &update) { @@ -1769,10 +1789,17 @@ void VoiceRecordBar::hideFast() { void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { using namespace ::Media::Capture; if (type == StopType::Cancel) { + if (_videoRecorder) { + _videoRecorder->setPaused(true); + _videoRecorder->hide(); + } instance()->stop(crl::guard(this, [=](Result &&data) { _cancelRequests.fire({}); })); } else if (type == StopType::Listen) { + if (_videoRecorder) { + _videoRecorder->setPaused(true); + } instance()->pause(true, crl::guard(this, [=](Result &&data) { if (data.bytes.isEmpty()) { // Close everything. @@ -1795,6 +1822,29 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { // _lockShowing = false; })); } else if (type == StopType::Send) { + if (_videoRecorder) { + const auto weak = Ui::MakeWeak(this); + _videoRecorder->hide([=](Ui::RoundVideoResult data) { + crl::on_main([=, data = std::move(data)]() mutable { + if (weak) { + window()->raise(); + window()->activateWindow(); + const auto options = Api::SendOptions{ + .ttlSeconds = (ttlBeforeHide + ? std::numeric_limits<int>::max() + : 0), + }; + _sendVoiceRequests.fire({ + data.content, + VoiceWaveform{}, + data.duration, + options, + true, + }); + } + }); + }); + } instance()->stop(crl::guard(this, [=](Result &&data) { if (data.bytes.isEmpty()) { // Close everything. @@ -2094,4 +2144,40 @@ void VoiceRecordBar::showDiscardBox( _warningShown = true; } +bool VoiceRecordBar::createVideoRecorder() { + if (_videoRecorder) { + return true; + } + const auto hidden = [=](not_null<Ui::RoundVideoRecorder*> which) { + if (_videoRecorder.get() == which) { + _videoRecorder = nullptr; + } + _videoHiding.erase( + ranges::remove( + _videoHiding, + which.get(), + &std::unique_ptr<Ui::RoundVideoRecorder>::get), + end(_videoHiding)); + }; + auto capturer = Core::App().calls().getVideoCapture(); + auto track = std::make_shared<Webrtc::VideoTrack>( + Webrtc::VideoState::Active); + capturer->setOutput(track->sink()); + capturer->setPreferredAspectRatio(1.); + _videoCapturerLifetime = track->stateValue( + ) | rpl::start_with_next([=](Webrtc::VideoState state) { + capturer->setState((state == Webrtc::VideoState::Active) + ? tgcalls::VideoState::Active + : tgcalls::VideoState::Inactive); + }); + _videoRecorder = std::make_unique<Ui::RoundVideoRecorder>( + Ui::RoundVideoRecorderDescriptor{ + .container = _outerContainer, + .hidden = hidden, + .capturer = std::move(capturer), + .track = std::move(track), + }); + return true; +} + } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h index 332bf5b1f..92ac2e6f8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h @@ -24,6 +24,7 @@ struct RecordBar; namespace Ui { class AbstractButton; class SendButton; +class RoundVideoRecorder; } // namespace Ui namespace Window { @@ -124,13 +125,10 @@ private: void recordUpdated(quint16 level, int samples); - [[nodiscard]] bool recordingAnimationCallback(crl::time now); - void stop(bool send); void stopRecording(StopType type, bool ttlBeforeHide = false); void visibilityAnimate(bool show, Fn<void()> &&callback); - [[nodiscard]] bool showRecordButton() const; void drawDuration(QPainter &p); void drawRedCircle(QPainter &p); void drawMessage(QPainter &p, float64 recordActive); @@ -153,6 +151,8 @@ private: [[nodiscard]] bool peekTTLState() const; [[nodiscard]] bool takeTTLState() const; + [[nodiscard]] bool createVideoRecorder(); + const style::RecordBar &_st; const not_null<Ui::RpWidget*> _outerContainer; const std::shared_ptr<ChatHelpers::Show> _show; @@ -195,6 +195,11 @@ private: bool _recordingTipRequired = false; bool _lockFromBottom = false; + std::unique_ptr<Ui::RoundVideoRecorder> _videoRecorder; + std::vector<std::unique_ptr<Ui::RoundVideoRecorder>> _videoHiding; + rpl::lifetime _videoCapturerLifetime; + bool _recordingVideo = false; + const style::font &_cancelFont; rpl::lifetime _recordingLifetime; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 1ae6f36c5..107e4bf7f 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -1224,6 +1224,7 @@ void RepliesWidget::sendVoice(ComposeControls::VoiceToSend &&data) { data.bytes, data.waveform, data.duration, + data.video, std::move(action)); _composeControls->cancelReplyMessage(); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index cb5352f0b..31b31d01d 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -57,29 +57,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { ScheduledMemento::ScheduledMemento(not_null<History*> history) -: _history(history) -, _forumTopic(nullptr) { + : _history(history) + , _forumTopic(nullptr) { const auto list = _history->session().scheduledMessages().list(_history); if (!list.ids.empty()) { - _list.setScrollTopState({ .item = { .fullId = list.ids.front() } }); + _list.setScrollTopState({ .item = {.fullId = list.ids.front() } }); } } ScheduledMemento::ScheduledMemento(not_null<Data::ForumTopic*> forumTopic) -: _history(forumTopic->owningHistory()) -, _forumTopic(forumTopic) { + : _history(forumTopic->owningHistory()) + , _forumTopic(forumTopic) { const auto list = _history->session().scheduledMessages().list( _forumTopic); if (!list.ids.empty()) { - _list.setScrollTopState({ .item = { .fullId = list.ids.front() } }); + _list.setScrollTopState({ .item = {.fullId = list.ids.front() } }); } } object_ptr<Window::SectionWidget> ScheduledMemento::createWidget( - QWidget *parent, - not_null<Window::SessionController*> controller, - Window::Column column, - const QRect &geometry) { + QWidget *parent, + not_null<Window::SessionController*> controller, + Window::Column column, + const QRect &geometry) { if (column == Window::Column::Third) { return nullptr; } @@ -97,30 +97,30 @@ ScheduledWidget::ScheduledWidget( not_null<Window::SessionController*> controller, not_null<History*> history, const Data::ForumTopic *forumTopic) -: Window::SectionWidget(parent, controller, history->peer) -, WindowListDelegate(controller) -, _show(controller->uiShow()) -, _history(history) -, _forumTopic(forumTopic) -, _scroll( - this, - controller->chatStyle()->value(lifetime(), st::historyScroll), - false) -, _topBar(this, controller) -, _topBarShadow(this) -, _composeControls(std::make_unique<ComposeControls>( - this, - ComposeControlsDescriptor{ - .show = controller->uiShow(), - .unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) { - listShowPremiumToast(emoji); - }, - .mode = ComposeControls::Mode::Scheduled, - .sendMenuDetails = [] { return SendMenu::Details(); }, - .regularWindow = controller, - .stickerOrEmojiChosen = controller->stickerOrEmojiChosen(), - })) -, _cornerButtons( + : Window::SectionWidget(parent, controller, history->peer) + , WindowListDelegate(controller) + , _show(controller->uiShow()) + , _history(history) + , _forumTopic(forumTopic) + , _scroll( + this, + controller->chatStyle()->value(lifetime(), st::historyScroll), + false) + , _topBar(this, controller) + , _topBarShadow(this) + , _composeControls(std::make_unique<ComposeControls>( + this, + ComposeControlsDescriptor{ + .show = controller->uiShow(), + .unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) { + listShowPremiumToast(emoji); + }, + .mode = ComposeControls::Mode::Scheduled, + .sendMenuDetails = [] { return SendMenu::Details(); }, + .regularWindow = controller, + .stickerOrEmojiChosen = controller->stickerOrEmojiChosen(), + })) + , _cornerButtons( _scroll.data(), controller->chatStyle(), static_cast<HistoryView::CornerButtonsDelegate*>(this)) { @@ -209,83 +209,83 @@ ScheduledWidget::~ScheduledWidget() = default; void ScheduledWidget::setupComposeControls() { auto writeRestriction = _forumTopic ? [&] { - auto topicWriteRestrictions = rpl::single( - ) | rpl::then(session().changes().topicUpdates( - Data::TopicUpdate::Flag::Closed - ) | rpl::filter([=](const Data::TopicUpdate &update) { - return (update.topic->history() == _history) - && (update.topic->rootId() == _forumTopic->rootId()); - }) | rpl::to_empty) | rpl::map([=] { - return (!_forumTopic - || _forumTopic->canToggleClosed() - || !_forumTopic->closed()) - ? std::optional<QString>() - : tr::lng_forum_topic_closed(tr::now); - }); - return rpl::combine( - session().changes().peerFlagsValue( - _history->peer, - Data::PeerUpdate::Flag::Rights), - Data::CanSendAnythingValue(_history->peer), - std::move(topicWriteRestrictions) - ) | rpl::map([=]( - auto, - auto, - std::optional<QString> topicRestriction) { - const auto allWithoutPolls = Data::AllSendRestrictions() - & ~ChatRestriction::SendPolls; - const auto canSendAnything = Data::CanSendAnyOf( - _forumTopic, - allWithoutPolls); - const auto restriction = Data::RestrictionError( - _history->peer, - ChatRestriction::SendOther); - auto text = !canSendAnything - ? (restriction - ? restriction - : topicRestriction - ? std::move(topicRestriction) - : tr::lng_group_not_accessible(tr::now)) + auto topicWriteRestrictions = rpl::single( + ) | rpl::then(session().changes().topicUpdates( + Data::TopicUpdate::Flag::Closed + ) | rpl::filter([=](const Data::TopicUpdate &update) { + return (update.topic->history() == _history) + && (update.topic->rootId() == _forumTopic->rootId()); + }) | rpl::to_empty) | rpl::map([=] { + return (!_forumTopic + || _forumTopic->canToggleClosed() + || !_forumTopic->closed()) + ? std::optional<QString>() + : tr::lng_forum_topic_closed(tr::now); + }); + return rpl::combine( + session().changes().peerFlagsValue( + _history->peer, + Data::PeerUpdate::Flag::Rights), + Data::CanSendAnythingValue(_history->peer), + std::move(topicWriteRestrictions) + ) | rpl::map([=]( + auto, + auto, + std::optional<QString> topicRestriction) { + const auto allWithoutPolls = Data::AllSendRestrictions() + & ~ChatRestriction::SendPolls; + const auto canSendAnything = Data::CanSendAnyOf( + _forumTopic, + allWithoutPolls); + const auto restriction = Data::RestrictionError( + _history->peer, + ChatRestriction::SendOther); + auto text = !canSendAnything + ? (restriction + ? restriction : topicRestriction ? std::move(topicRestriction) - : std::optional<QString>(); - return text ? Controls::WriteRestriction{ - .text = std::move(*text), - .type = Controls::WriteRestrictionType::Rights, - } : Controls::WriteRestriction(); - }) | rpl::type_erased(); - }() + : tr::lng_group_not_accessible(tr::now)) + : topicRestriction + ? std::move(topicRestriction) + : std::optional<QString>(); + return text ? Controls::WriteRestriction{ + .text = std::move(*text), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); + }) | rpl::type_erased(); + }() : [&] { - return rpl::combine( - session().changes().peerFlagsValue( - _history->peer, - Data::PeerUpdate::Flag::Rights), - Data::CanSendAnythingValue(_history->peer) - ) | rpl::map([=] { - const auto allWithoutPolls = Data::AllSendRestrictions() - & ~ChatRestriction::SendPolls; - const auto canSendAnything = Data::CanSendAnyOf( - _history->peer, - allWithoutPolls, - false); - const auto restriction = Data::RestrictionError( - _history->peer, - ChatRestriction::SendOther); - auto text = !canSendAnything - ? (restriction - ? restriction - : tr::lng_group_not_accessible(tr::now)) - : std::optional<QString>(); - return text ? Controls::WriteRestriction{ - .text = std::move(*text), - .type = Controls::WriteRestrictionType::Rights, - } : Controls::WriteRestriction(); - }) | rpl::type_erased(); - }(); + return rpl::combine( + session().changes().peerFlagsValue( + _history->peer, + Data::PeerUpdate::Flag::Rights), + Data::CanSendAnythingValue(_history->peer) + ) | rpl::map([=] { + const auto allWithoutPolls = Data::AllSendRestrictions() + & ~ChatRestriction::SendPolls; + const auto canSendAnything = Data::CanSendAnyOf( + _history->peer, + allWithoutPolls, + false); + const auto restriction = Data::RestrictionError( + _history->peer, + ChatRestriction::SendOther); + auto text = !canSendAnything + ? (restriction + ? restriction + : tr::lng_group_not_accessible(tr::now)) + : std::optional<QString>(); + return text ? Controls::WriteRestriction{ + .text = std::move(*text), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); + }) | rpl::type_erased(); + }(); _composeControls->setHistory({ .history = _history.get(), .writeRestriction = std::move(writeRestriction), - }); + }); _composeControls->height( ) | rpl::start_with_next([=] { @@ -308,7 +308,7 @@ void ScheduledWidget::setupComposeControls() { _composeControls->sendVoiceRequests( ) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) { - sendVoice(data.bytes, data.waveform, data.duration); + sendVoice(std::move(data)); }, lifetime()); _composeControls->sendCommandRequests( @@ -393,8 +393,8 @@ void ScheduledWidget::setupComposeControls() { }, lifetime()); _composeControls->setMimeDataHook([=]( - not_null<const QMimeData*> data, - Ui::InputField::MimeAction action) { + not_null<const QMimeData*> data, + Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { @@ -426,7 +426,7 @@ void ScheduledWidget::chooseAttach() { const auto filter = FileDialog::AllOrImagesFilter(); FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( - FileDialog::OpenResult &&result) { + FileDialog::OpenResult &&result) { if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { return; } @@ -434,7 +434,7 @@ void ScheduledWidget::chooseAttach() { if (!result.remoteContent.isEmpty()) { auto read = Images::Read({ .content = result.remoteContent, - }); + }); if (!read.image.isNull() && !read.animated) { confirmSendingFiles( std::move(read.image), @@ -454,9 +454,9 @@ void ScheduledWidget::chooseAttach() { } bool ScheduledWidget::confirmSendingFiles( - not_null<const QMimeData*> data, - std::optional<bool> overrideSendImagesAsPhotos, - const QString &insertTextOnCancel) { + not_null<const QMimeData*> data, + std::optional<bool> overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { const auto hasImage = data->hasImage(); const auto premium = controller()->session().user()->isPremium(); @@ -488,8 +488,8 @@ bool ScheduledWidget::confirmSendingFiles( } bool ScheduledWidget::confirmSendingFiles( - Ui::PreparedList &&list, - const QString &insertTextOnCancel) { + Ui::PreparedList &&list, + const QString &insertTextOnCancel) { if (_composeControls->confirmMediaEdit(list)) { return true; } else if (showSendingFilesError(list)) { @@ -507,11 +507,11 @@ bool ScheduledWidget::confirmSendingFiles( SendMenu::Details()); box->setConfirmedCallback(crl::guard(this, [=]( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { sendingFilesConfirmed( std::move(list), way, @@ -529,11 +529,11 @@ bool ScheduledWidget::confirmSendingFiles( } void ScheduledWidget::sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { Expects(list.filesToProcess.empty()); if (showSendingFilesError(list, way.sendImagesAsPhotos())) { @@ -565,10 +565,10 @@ void ScheduledWidget::sendingFilesConfirmed( } bool ScheduledWidget::confirmSendingFiles( - QImage &&image, - QByteArray &&content, - std::optional<bool> overrideSendImagesAsPhotos, - const QString &insertTextOnCancel) { + QImage &&image, + QByteArray &&content, + std::optional<bool> overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { if (image.isNull()) { return false; } @@ -604,8 +604,8 @@ void ScheduledWidget::checkReplyReturns() { } void ScheduledWidget::uploadFile( - const QByteArray &fileContent, - SendMediaType type) { + const QByteArray &fileContent, + SendMediaType type) { const auto callback = [=](Api::SendOptions options) { session().api().sendFile( fileContent, @@ -617,13 +617,13 @@ void ScheduledWidget::uploadFile( } bool ScheduledWidget::showSendingFilesError( - const Ui::PreparedList &list) const { + const Ui::PreparedList &list) const { return showSendingFilesError(list, std::nullopt); } bool ScheduledWidget::showSendingFilesError( - const Ui::PreparedList &list, - std::optional<bool> compress) const { + const Ui::PreparedList &list, + std::optional<bool> compress) const { const auto text = [&] { using Error = Ui::PreparedList::Error; const auto peer = _history->peer; @@ -656,7 +656,7 @@ bool ScheduledWidget::showSendingFilesError( } Api::SendAction ScheduledWidget::prepareSendAction( - Api::SendOptions options) const { + Api::SendOptions options) const { auto result = Api::SendAction(_history, options); result.options.sendAs = _composeControls->sendAsPeer(); if (_forumTopic) { @@ -716,26 +716,22 @@ void ScheduledWidget::send(Api::SendOptions options) { _composeControls->focus(); } -void ScheduledWidget::sendVoice( - QByteArray bytes, - VoiceWaveform waveform, - crl::time duration) { +void ScheduledWidget::sendVoice(const Controls::VoiceToSend &data) { const auto callback = [=](Api::SendOptions options) { - sendVoice(bytes, waveform, duration, options); + sendVoice(base::duplicate(data), options); }; controller()->show( PrepareScheduleBox(this, _show, sendMenuDetails(), callback)); } void ScheduledWidget::sendVoice( - QByteArray bytes, - VoiceWaveform waveform, - crl::time duration, + const Controls::VoiceToSend &data, Api::SendOptions options) { session().api().sendVoiceMessage( - bytes, - waveform, - duration, + data.bytes, + data.waveform, + data.duration, + data.video, prepareSendAction(options)); _composeControls->clearListenState(); } diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index 0a7ff39b0..290b11c73 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -47,6 +47,10 @@ namespace InlineBots { class Result; } // namespace InlineBots +namespace HistoryView::Controls { +struct VoiceToSend; +} // namespace HistoryView::Controls + namespace HistoryView { class Element; @@ -207,14 +211,9 @@ private: Api::SendOptions options) const; void send(); void send(Api::SendOptions options); + void sendVoice(const Controls::VoiceToSend &data); void sendVoice( - QByteArray bytes, - VoiceWaveform waveform, - crl::time duration); - void sendVoice( - QByteArray bytes, - VoiceWaveform waveform, - crl::time duration, + const Controls::VoiceToSend &data, Api::SendOptions options); void edit( not_null<HistoryItem*> item, diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp index 1bbf9b10c..cd567f4fd 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp +++ b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp @@ -88,13 +88,15 @@ public: void start( Webrtc::DeviceResolvedId id, Fn<void(Update)> updated, - Fn<void()> error); + Fn<void()> error, + Fn<void(Chunk)> externalProcessing); void stop(Fn<void(Result&&)> callback = nullptr); void pause(bool value, Fn<void(Result&&)> callback); private: void process(); + bool initializeFFmpeg(); [[nodiscard]] bool processFrame(int32 offset, int32 framesize); void fail(); @@ -104,6 +106,7 @@ private: // Returns number of packets written or -1 on error [[nodiscard]] int writePackets(); + Fn<void(Chunk)> _externalProcessing; Fn<void(Update)> _updated; Fn<void()> _error; @@ -131,7 +134,7 @@ Instance::Instance() : _inner(std::make_unique<Inner>(&_thread)) { _thread.start(); } -void Instance::start() { +void Instance::start(Fn<void(Chunk)> externalProcessing) { _updates.fire_done(); const auto id = Audio::Current().captureDeviceId(); InvokeQueued(_inner.get(), [=] { @@ -143,7 +146,7 @@ void Instance::start() { crl::on_main(this, [=] { _updates.fire_error({}); }); - }); + }, externalProcessing); crl::on_main(this, [=] { _started = true; }); @@ -304,7 +307,9 @@ void Instance::Inner::fail() { void Instance::Inner::start( Webrtc::DeviceResolvedId id, Fn<void(Update)> updated, - Fn<void()> error) { + Fn<void()> error, + Fn<void(Chunk)> externalProcessing) { + _externalProcessing = std::move(externalProcessing); _updated = std::move(updated); _error = std::move(error); if (_paused) { @@ -329,8 +334,19 @@ void Instance::Inner::start( d->device = nullptr; fail(); return; + } else if (!_externalProcessing) { + if (!initializeFFmpeg()) { + fail(); + return; + } } + _timer.callEach(50); + _captured.clear(); + _captured.reserve(kCaptureBufferSlice); + DEBUG_LOG(("Audio Capture: started!")); +} +bool Instance::Inner::initializeFFmpeg() { // Create encoding context d->ioBuffer = (uchar*)av_malloc(FFmpeg::kAVBlockSize); @@ -347,14 +363,12 @@ void Instance::Inner::start( } if (!fmt) { LOG(("Audio Error: Unable to find opus AVOutputFormat for capture")); - fail(); - return; + return false; } if ((res = avformat_alloc_output_context2(&d->fmtContext, (AVOutputFormat*)fmt, 0, 0)) < 0) { LOG(("Audio Error: Unable to avformat_alloc_output_context2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } d->fmtContext->pb = d->ioContext; d->fmtContext->flags |= AVFMT_FLAG_CUSTOM_IO; @@ -364,21 +378,18 @@ void Instance::Inner::start( d->codec = avcodec_find_encoder(fmt->audio_codec); if (!d->codec) { LOG(("Audio Error: Unable to avcodec_find_encoder for capture")); - fail(); - return; + return false; } d->stream = avformat_new_stream(d->fmtContext, d->codec); if (!d->stream) { LOG(("Audio Error: Unable to avformat_new_stream for capture")); - fail(); - return; + return false; } d->stream->id = d->fmtContext->nb_streams - 1; d->codecContext = avcodec_alloc_context3(d->codec); if (!d->codecContext) { LOG(("Audio Error: Unable to avcodec_alloc_context3 for capture")); - fail(); - return; + return false; } av_opt_set_int(d->codecContext, "refcounted_frames", 1, 0); @@ -401,8 +412,7 @@ void Instance::Inner::start( // Open audio stream if ((res = avcodec_open2(d->codecContext, d->codec, nullptr)) < 0) { LOG(("Audio Error: Unable to avcodec_open2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } // Alloc source samples @@ -443,39 +453,27 @@ void Instance::Inner::start( #endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT if (res < 0 || !d->swrContext) { LOG(("Audio Error: Unable to swr_alloc_set_opts2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } else if ((res = swr_init(d->swrContext)) < 0) { LOG(("Audio Error: Unable to swr_init for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } - d->maxDstSamples = d->srcSamples; if ((res = av_samples_alloc_array_and_samples(&d->dstSamplesData, 0, d->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0)) < 0) { LOG(("Audio Error: Unable to av_samples_alloc_array_and_samples for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } d->dstSamplesSize = av_samples_get_buffer_size(0, d->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0); - if ((res = avcodec_parameters_from_context(d->stream->codecpar, d->codecContext)) < 0) { LOG(("Audio Error: Unable to avcodec_parameters_from_context for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } - // Write file header if ((res = avformat_write_header(d->fmtContext, 0)) < 0) { LOG(("Audio Error: Unable to avformat_write_header for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } - - _timer.callEach(50); - _captured.clear(); - _captured.reserve(kCaptureBufferSlice); - DEBUG_LOG(("Audio Capture: started!")); + return true; } void Instance::Inner::pause(bool value, Fn<void(Result&&)> callback) { @@ -559,7 +557,7 @@ void Instance::Inner::stop(Fn<void(Result&&)> callback) { _captured = QByteArray(); // Finish stream - if (needResult && hadDevice) { + if (needResult && hadDevice && d->fmtContext) { av_write_trailer(d->fmtContext); } @@ -658,6 +656,13 @@ void Instance::Inner::process() { if (ErrorHappened(d->device)) { fail(); return; + } else if (_externalProcessing) { + _externalProcessing({ + .finished = crl::now(), + .samples = base::take(_captured), + .frequency = kCaptureFrequency, + }); + return; } // Count new recording level and update view diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.h b/Telegram/SourceFiles/media/audio/media_audio_capture.h index 2b46bd7fa..fe03dd89c 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture.h +++ b/Telegram/SourceFiles/media/audio/media_audio_capture.h @@ -7,10 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include <QtCore/QThread> #include <QtCore/QTimer> -struct AVFrame; - namespace Media { namespace Capture { @@ -19,6 +18,12 @@ struct Update { ushort level = 0; }; +struct Chunk { + crl::time finished = 0; + QByteArray samples; + int frequency = 0; +}; + struct Result; void Start(); @@ -45,7 +50,7 @@ public: return _started.changes(); } - void start(); + void start(Fn<void(Chunk)> externalProcessing = nullptr); void stop(Fn<void(Result&&)> callback = nullptr); void pause(bool value, Fn<void(Result&&)> callback); diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 136c77b7a..c034d61dc 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -244,6 +244,7 @@ void ReplyArea::sendVoice(VoiceToSend &&data) { data.bytes, data.waveform, data.duration, + data.video, std::move(action)); _controls->clearListenState(); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 1610aafee..d1dfbbad5 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -1200,6 +1200,7 @@ void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) { data.bytes, data.waveform, data.duration, + data.video, std::move(action)); _composeControls->cancelReplyMessage(); diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 79c4af401..9daeaecdd 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -498,6 +498,7 @@ FileLoadTask::FileLoadTask( const QByteArray &voice, crl::time duration, const VoiceWaveform &waveform, + bool video, const FileLoadTo &to, const TextWithTags &caption) : _id(base::RandomValue<uint64>()) @@ -507,7 +508,7 @@ FileLoadTask::FileLoadTask( , _content(voice) , _duration(duration) , _waveform(waveform) -, _type(SendMediaType::Audio) +, _type(video ? SendMediaType::Round : SendMediaType::Audio) , _caption(caption) { } @@ -696,6 +697,7 @@ void FileLoadTask::process(Args &&args) { auto isSong = false; auto isVideo = false; auto isVoice = (_type == SendMediaType::Audio); + auto isRound = (_type == SendMediaType::Round); auto isSticker = false; auto fullimage = QImage(); @@ -711,7 +713,7 @@ void FileLoadTask::process(Args &&args) { // Voice sending is supported only from memory for now. // Because for voice we force mime type and don't read MediaInformation. // For a real file we always read mime type and read MediaInformation. - Assert(!isVoice); + Assert(!isVoice && !isRound); filesize = info.size(); filename = info.fileName(); @@ -736,6 +738,9 @@ void FileLoadTask::process(Args &&args) { if (isVoice) { filename = filedialogDefaultName(u"audio"_q, u".ogg"_q, QString(), true); filemime = "audio/ogg"; + } else if (isRound) { + filename = filedialogDefaultName(u"round"_q, u".mp4"_q, QString(), true); + filemime = "video/mp4"; } else { if (_information) { if (auto image = std::get_if<Ui::PreparedFileInformation::Image>( @@ -815,7 +820,41 @@ void FileLoadTask::process(Args &&args) { auto photo = MTP_photoEmpty(MTP_long(0)); auto document = MTP_documentEmpty(MTP_long(0)); - if (!isVoice) { + if (isRound) { + _information = readMediaInformation(u"video/mp4"_q); + if (auto video = std::get_if<Ui::PreparedFileInformation::Video>( + &_information->media)) { + isVideo = true; + auto coverWidth = video->thumbnail.width(); + auto coverHeight = video->thumbnail.height(); + if (video->isGifv && !_album) { + attributes.push_back(MTP_documentAttributeAnimated()); + } + auto flags = MTPDdocumentAttributeVideo::Flags( + MTPDdocumentAttributeVideo::Flag::f_round_message); + if (video->supportsStreaming) { + flags |= MTPDdocumentAttributeVideo::Flag::f_supports_streaming; + } + const auto realSeconds = video->duration / 1000.; + attributes.push_back(MTP_documentAttributeVideo( + MTP_flags(flags), + MTP_double(realSeconds), + MTP_int(coverWidth), + MTP_int(coverHeight), + MTPint(), // preload_prefix_size + MTPdouble(), // video_start_ts + MTPstring())); // video_codec + + if (args.generateGoodThumbnail) { + goodThumbnail = video->thumbnail; + { + QBuffer buffer(&goodThumbnailBytes); + goodThumbnail.save(&buffer, "JPG", kThumbnailQuality); + } + } + thumbnail = PrepareFileThumbnail(std::move(video->thumbnail)); + } + } else if (!isVoice) { if (!_information) { _information = readMediaInformation(filemime); filemime = _information->filemime; @@ -869,7 +908,7 @@ void FileLoadTask::process(Args &&args) { } } - if (!fullimage.isNull() && fullimage.width() > 0 && !isSong && !isVideo && !isVoice) { + if (!fullimage.isNull() && fullimage.width() > 0 && !isSong && !isVideo && !isVoice && !isRound) { auto w = fullimage.width(), h = fullimage.height(); attributes.push_back(MTP_documentAttributeImageSize(MTP_int(w), MTP_int(h))); diff --git a/Telegram/SourceFiles/storage/localimageloader.h b/Telegram/SourceFiles/storage/localimageloader.h index d4e99177f..ed0dc1c47 100644 --- a/Telegram/SourceFiles/storage/localimageloader.h +++ b/Telegram/SourceFiles/storage/localimageloader.h @@ -31,6 +31,7 @@ extern const char kOptionSendLargePhotos[]; enum class SendMediaType { Photo, Audio, + Round, File, ThemeFile, Secure, @@ -231,6 +232,7 @@ public: const QByteArray &voice, crl::time duration, const VoiceWaveform &waveform, + bool video, const FileLoadTo &to, const TextWithTags &caption); ~FileLoadTask(); diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp new file mode 100644 index 000000000..09b60bb7b --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp @@ -0,0 +1,649 @@ +/* +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 "ui/controls/round_video_recorder.h" + +#include "base/debug_log.h" +#include "ffmpeg/ffmpeg_utility.h" +#include "media/audio/media_audio_capture.h" +#include "ui/painter.h" +#include "ui/rp_widget.h" +#include "webrtc/webrtc_video_track.h" +#include "styles/style_chat_helpers.h" + +namespace Ui { +namespace { + +constexpr auto kSide = 400; +constexpr auto kOutputFilename = "C:\\Tmp\\TestVideo\\output.mp4"; + +using namespace FFmpeg; + +} // namespace + +class RoundVideoRecorder::Private final { +public: + Private(crl::weak_on_queue<Private> weak); + ~Private(); + + void push(int64 mcstimestamp, const QImage &frame); + void push(const Media::Capture::Chunk &chunk); + + [[nodiscard]] RoundVideoResult finish(); + +private: + static int Write(void *opaque, uint8_t *buf, int buf_size); + static int64_t Seek(void *opaque, int64_t offset, int whence); + + int write(uint8_t *buf, int buf_size); + int64_t seek(int64_t offset, int whence); + + const crl::weak_on_queue<Private> _weak; + + FormatPointer _format; + + AVStream *_videoStream = nullptr; + CodecPointer _videoCodec; + FramePointer _videoFrame; + SwscalePointer _swsContext; + int64_t _videoPts = 0; + + // This is the first recorded frame timestamp in microseconds. + int64_t _videoFirstTimestamp = -1; + + // Audio-related members + AVStream *_audioStream = nullptr; + CodecPointer _audioCodec; + FramePointer _audioFrame; + SwresamplePointer _swrContext; + QByteArray _audioTail; + int64_t _audioPts = 0; + int _audioChannels = 0; + + // Those timestamps are in 'ms' used for sync between audio and video. + crl::time _firstAudioChunkFinished = 0; + crl::time _firstVideoFrameTime = 0; + + QByteArray _result; + int64_t _resultOffset = 0; + crl::time _resultDuration = 0; + + void initEncoding(); + bool initVideo(); + bool initAudio(); + void deinitEncoding(); + void finishEncoding(); + + void encodeVideoFrame(int64 mcstimestamp, const QImage &frame); + void encodeAudioFrame(const Media::Capture::Chunk &chunk); + bool writeFrame( + const FramePointer &frame, + const CodecPointer &codec, + AVStream *stream); + +}; + +RoundVideoRecorder::Private::Private(crl::weak_on_queue<Private> weak) +: _weak(std::move(weak)) { + initEncoding(); +} + +RoundVideoRecorder::Private::~Private() { + finishEncoding(); + + QFile file(kOutputFilename); + if (file.open(QIODevice::WriteOnly)) { + file.write(_result); + } +} + +int RoundVideoRecorder::Private::Write(void *opaque, uint8_t *buf, int buf_size) { + return static_cast<Private*>(opaque)->write(buf, buf_size); +} + +int64_t RoundVideoRecorder::Private::Seek(void *opaque, int64_t offset, int whence) { + return static_cast<Private*>(opaque)->seek(offset, whence); +} + +int RoundVideoRecorder::Private::write(uint8_t *buf, int buf_size) { + if (const auto total = _resultOffset + int64(buf_size)) { + const auto size = int64(_result.size()); + constexpr auto kReserve = 1024 * 1024; + _result.reserve((total / kReserve) * kReserve); + const auto overwrite = std::min( + size - _resultOffset, + int64(buf_size)); + if (overwrite) { + memcpy(_result.data() + _resultOffset, buf, overwrite); + } + if (const auto append = buf_size - overwrite) { + _result.append( + reinterpret_cast<const char*>(buf) + overwrite, + append); + } + _resultOffset += buf_size; + } + return buf_size; +} + +int64_t RoundVideoRecorder::Private::seek(int64_t offset, int whence) { + const auto checkedSeek = [&](int64_t offset) { + if (offset < 0 || offset > int64(_result.size())) { + return int64(-1); + } + return (_resultOffset = offset); + }; + switch (whence) { + case SEEK_SET: return checkedSeek(offset); + case SEEK_CUR: return checkedSeek(_resultOffset + offset); + case SEEK_END: return checkedSeek(int64(_result.size()) + offset); + case AVSEEK_SIZE: return int64(_result.size()); + } + return -1; +} + +void RoundVideoRecorder::Private::initEncoding() { + _format = MakeWriteFormatPointer( + static_cast<void*>(this), + nullptr, + &Private::Write, + &Private::Seek, + "mp4"_q); + + if (!initVideo() || !initAudio()) { + deinitEncoding(); + return; + } + + const auto error = AvErrorWrap(avformat_write_header( + _format.get(), + nullptr)); + if (error) { + LogError("avformat_write_header", error); + deinitEncoding(); + } +} + +bool RoundVideoRecorder::Private::initVideo() { + if (!_format) { + return false; + } + + const auto videoCodec = avcodec_find_encoder_by_name("libopenh264"); + if (!videoCodec) { + LogError("avcodec_find_encoder_by_name", "libopenh264"); + return false; + } + + _videoStream = avformat_new_stream(_format.get(), videoCodec); + if (!_videoStream) { + LogError("avformat_new_stream", "libopenh264"); + return false; + } + + _videoCodec = CodecPointer(avcodec_alloc_context3(videoCodec)); + if (!_videoCodec) { + LogError("avcodec_alloc_context3", "libopenh264"); + return false; + } + + _videoCodec->codec_id = videoCodec->id; + _videoCodec->codec_type = AVMEDIA_TYPE_VIDEO; + _videoCodec->width = kSide; + _videoCodec->height = kSide; + _videoCodec->time_base = AVRational{ 1, 1'000'000 }; // Microseconds. + _videoCodec->framerate = AVRational{ 0, 1 }; // Variable frame rate. + _videoCodec->pix_fmt = AV_PIX_FMT_YUV420P; + _videoCodec->bit_rate = 5 * 1024 * 1024; // 5Mbps + + auto error = AvErrorWrap(avcodec_open2( + _videoCodec.get(), + videoCodec, + nullptr)); + if (error) { + LogError("avcodec_open2", error, "libopenh264"); + return false; + } + + error = AvErrorWrap(avcodec_parameters_from_context( + _videoStream->codecpar, + _videoCodec.get())); + if (error) { + LogError("avcodec_parameters_from_context", error, "libopenh264"); + return false; + } + + _videoFrame = MakeFramePointer(); + if (!_videoFrame) { + return false; + } + + _videoFrame->format = _videoCodec->pix_fmt; + _videoFrame->width = _videoCodec->width; + _videoFrame->height = _videoCodec->height; + + error = AvErrorWrap(av_frame_get_buffer(_videoFrame.get(), 0)); + if (error) { + LogError("av_frame_get_buffer", error, "libopenh264"); + return false; + } + + return true; +} + +bool RoundVideoRecorder::Private::initAudio() { + if (!_format) { + return false; + } + + const auto audioCodec = avcodec_find_encoder(AV_CODEC_ID_AAC); + if (!audioCodec) { + LogError("avcodec_find_encoder", "AAC"); + return false; + } + + _audioStream = avformat_new_stream(_format.get(), audioCodec); + if (!_audioStream) { + LogError("avformat_new_stream", "AAC"); + return false; + } + + _audioCodec = CodecPointer(avcodec_alloc_context3(audioCodec)); + if (!_audioCodec) { + LogError("avcodec_alloc_context3", "AAC"); + return false; + } + + _audioChannels = 1; + _audioCodec->sample_fmt = AV_SAMPLE_FMT_FLTP; + _audioCodec->bit_rate = 32000; + _audioCodec->sample_rate = 48000; +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + _audioCodec->ch_layout = AV_CHANNEL_LAYOUT_MONO; + _audioCodec->channels = _audioCodec->ch_layout.nb_channels; +#else + _audioCodec->channel_layout = AV_CH_LAYOUT_MONO; + _audioCodec->channels = _audioChannels; +#endif + + auto error = AvErrorWrap(avcodec_open2( + _audioCodec.get(), + audioCodec, + nullptr)); + if (error) { + LogError("avcodec_open2", error, "AAC"); + return false; + } + + error = AvErrorWrap(avcodec_parameters_from_context( + _audioStream->codecpar, + _audioCodec.get())); + if (error) { + LogError("avcodec_parameters_from_context", error, "AAC"); + return false; + } + +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + _swrContext = MakeSwresamplePointer( + &_audioCodec->ch_layout, + AV_SAMPLE_FMT_S16, + _audioCodec->sample_rate, + &_audioCodec->ch_layout, + _audioCodec->sample_fmt, + _audioCodec->sample_rate, + &_swrContext); +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + _swrContext = MakeSwresamplePointer( + &_audioCodec->ch_layout, + AV_SAMPLE_FMT_S16, + _audioCodec->sample_rate, + &_audioCodec->ch_layout, + _audioCodec->sample_fmt, + _audioCodec->sample_rate, + &_swrContext); +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + if (!_swrContext) { + return false; + } + + _audioFrame = MakeFramePointer(); + if (!_audioFrame) { + return false; + } + + _audioFrame->nb_samples = _audioCodec->frame_size; + _audioFrame->format = _audioCodec->sample_fmt; + _audioFrame->sample_rate = _audioCodec->sample_rate; +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + av_channel_layout_copy(&_audioFrame->ch_layout, &_audioCodec->ch_layout); +#else + _audioFrame->channel_layout = _audioCodec->channel_layout; + _audioFrame->channels = _audioCodec->channels; +#endif + + error = AvErrorWrap(av_frame_get_buffer(_audioFrame.get(), 0)); + if (error) { + LogError("av_frame_get_buffer", error, "AAC"); + return false; + } + + return true; +} + +void RoundVideoRecorder::Private::finishEncoding() { + if (_format + && writeFrame(nullptr, _videoCodec, _videoStream) + && writeFrame(nullptr, _audioCodec, _audioStream)) { + av_write_trailer(_format.get()); + } + deinitEncoding(); +} + +RoundVideoResult RoundVideoRecorder::Private::finish() { + if (!_format) { + return {}; + } + finishEncoding(); + return { + .content = _result, + .waveform = QByteArray(), + .duration = _resultDuration, + }; +}; + +void RoundVideoRecorder::Private::deinitEncoding() { + _swsContext = nullptr; + _videoCodec = nullptr; + _videoStream = nullptr; + _videoFrame = nullptr; + _swrContext = nullptr; + _audioCodec = nullptr; + _audioStream = nullptr; + _audioFrame = nullptr; + _format = nullptr; + + _videoFirstTimestamp = -1; + _videoPts = 0; + _audioPts = 0; +} + +void RoundVideoRecorder::Private::push( + int64 mcstimestamp, + const QImage &frame) { + if (!_format) { + return; + } else if (!_firstAudioChunkFinished) { + // Skip frames while we didn't start receiving audio. + return; + } else if (!_firstVideoFrameTime) { + _firstVideoFrameTime = crl::now(); + } + encodeVideoFrame(mcstimestamp, frame); +} + +void RoundVideoRecorder::Private::push(const Media::Capture::Chunk &chunk) { + if (!_format) { + return; + } else if (!_firstAudioChunkFinished || !_firstVideoFrameTime) { + _firstAudioChunkFinished = chunk.finished; + return; + } + // We get a chunk roughly every 50ms and need to encode it interleaved. + encodeAudioFrame(chunk); +} + +void RoundVideoRecorder::Private::encodeVideoFrame( + int64 mcstimestamp, + const QImage &frame) { + _swsContext = MakeSwscalePointer( + QSize(kSide, kSide), + AV_PIX_FMT_BGRA, + QSize(kSide, kSide), + AV_PIX_FMT_YUV420P, + &_swsContext); + if (!_swsContext) { + deinitEncoding(); + return; + } + + if (_videoFirstTimestamp == -1) { + _videoFirstTimestamp = mcstimestamp; + } + + const auto fwidth = frame.width(); + const auto fheight = frame.height(); + const auto fmin = std::min(fwidth, fheight); + const auto fx = (fwidth > fheight) ? (fwidth - fheight) / 2 : 0; + const auto fy = (fwidth < fheight) ? (fheight - fwidth) / 2 : 0; + const auto crop = QRect(fx, fy, fmin, fmin); + const auto cropped = frame.copy(crop).scaled( + kSide, + kSide, + Qt::KeepAspectRatio, + Qt::SmoothTransformation); + + // Convert QImage to RGB32 format +// QImage rgbImage = cropped.convertToFormat(QImage::Format_ARGB32); + + // Prepare source data + const uint8_t *srcSlice[1] = { cropped.constBits() }; + int srcStride[1] = { cropped.bytesPerLine() }; + + // Perform the color space conversion + sws_scale( + _swsContext.get(), + srcSlice, + srcStride, + 0, + kSide, + _videoFrame->data, + _videoFrame->linesize); + + _videoFrame->pts = mcstimestamp - _videoFirstTimestamp; + + LOG(("Audio At: %1").arg(_videoFrame->pts / 1'000'000.)); + if (!writeFrame(_videoFrame, _videoCodec, _videoStream)) { + return; + } +} + +void RoundVideoRecorder::Private::encodeAudioFrame(const Media::Capture::Chunk &chunk) { + if (_audioTail.isEmpty()) { + _audioTail = chunk.samples; + } else { + _audioTail.append(chunk.samples); + } + + const int inSamples = _audioTail.size() / sizeof(int16_t); + const uint8_t *inData = reinterpret_cast<const uint8_t*>(_audioTail.constData()); + int samplesProcessed = 0; + + while (samplesProcessed + _audioCodec->frame_size <= inSamples) { + int remainingSamples = inSamples - samplesProcessed; + int outSamples = av_rescale_rnd( + swr_get_delay(_swrContext.get(), 48000) + remainingSamples, + _audioCodec->sample_rate, + 48000, + AV_ROUND_UP); + + // Ensure we don't exceed the frame's capacity + outSamples = std::min(outSamples, _audioCodec->frame_size); + + const auto process = std::min(remainingSamples, outSamples); + auto dataptr = inData + samplesProcessed * sizeof(int16_t); + auto error = AvErrorWrap(swr_convert( + _swrContext.get(), + _audioFrame->data, + outSamples, + &dataptr, + process)); + + if (error) { + LogError("swr_convert", error); + deinitEncoding(); + return; + } + + // Update the actual number of samples in the frame + _audioFrame->nb_samples = error.code(); + + _audioFrame->pts = _audioPts; + _audioPts += _audioFrame->nb_samples; + + LOG(("Audio At: %1").arg(_audioFrame->pts / 48'000.)); + if (!writeFrame(_audioFrame, _audioCodec, _audioStream)) { + return; + } + + samplesProcessed += process; + } + const auto left = inSamples - samplesProcessed; + if (left > 0) { + memmove(_audioTail.data(), _audioTail.data() + samplesProcessed * sizeof(int16_t), left * sizeof(int16_t)); + _audioTail.resize(left * sizeof(int16_t)); + } else { + _audioTail.clear(); + } +} + +bool RoundVideoRecorder::Private::writeFrame( + const FramePointer &frame, + const CodecPointer &codec, + AVStream *stream) { + auto error = AvErrorWrap(avcodec_send_frame(codec.get(), frame.get())); + if (error) { + LogError("avcodec_send_frame", error); + deinitEncoding(); + return false; + } + + auto pkt = av_packet_alloc(); + const auto guard = gsl::finally([&] { + av_packet_free(&pkt); + }); + while (true) { + error = AvErrorWrap(avcodec_receive_packet(codec.get(), pkt)); + if (error.code() == AVERROR(EAGAIN)) { + return true; // Need more input + } else if (error.code() == AVERROR_EOF) { + return true; // Encoding finished + } else if (error) { + LogError("avcodec_receive_packet", error); + deinitEncoding(); + return false; + } + + pkt->stream_index = stream->index; + av_packet_rescale_ts(pkt, codec->time_base, stream->time_base); + + accumulate_max( + _resultDuration, + PtsToTimeCeil(pkt->pts, stream->time_base)); + + error = AvErrorWrap(av_interleaved_write_frame(_format.get(), pkt)); + if (error) { + LogError("av_interleaved_write_frame", error); + deinitEncoding(); + return false; + } + } + + return true; +} + +RoundVideoRecorder::RoundVideoRecorder( + RoundVideoRecorderDescriptor &&descriptor) +: _descriptor(std::move(descriptor)) +, _preview(std::make_unique<RpWidget>(_descriptor.container)) +, _private() { + setup(); +} + +RoundVideoRecorder::~RoundVideoRecorder() = default; + +Fn<void(Media::Capture::Chunk)> RoundVideoRecorder::audioChunkProcessor() { + return [weak = _private.weak()](Media::Capture::Chunk chunk) { + weak.with([copy = std::move(chunk)](Private &that) { + that.push(copy); + }); + }; +} + +void RoundVideoRecorder::hide(Fn<void(RoundVideoResult)> done) { + if (done) { + _private.with([done = std::move(done)](Private &that) { + done(that.finish()); + }); + } + + setPaused(true); + + _preview->hide(); + if (const auto onstack = _descriptor.hidden) { + onstack(this); + } +} + +void RoundVideoRecorder::setup() { + const auto raw = _preview.get(); + + const auto side = style::ConvertScale(kSide * 3 / 4); + _descriptor.container->sizeValue( + ) | rpl::start_with_next([=](QSize outer) { + raw->setGeometry( + style::centerrect( + QRect(QPoint(), outer), + QRect(0, 0, side, side))); + }, raw->lifetime()); + + raw->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(raw); + auto hq = PainterHighQualityEnabler(p); + + auto info = _descriptor.track->frameWithInfo(true); + if (!info.original.isNull()) { + const auto owidth = info.original.width(); + const auto oheight = info.original.height(); + const auto omin = std::min(owidth, oheight); + const auto ox = (owidth > oheight) ? (owidth - oheight) / 2 : 0; + const auto oy = (owidth < oheight) ? (oheight - owidth) / 2 : 0; + const auto from = QRect(ox, oy, omin, omin); + p.drawImage(QRect(0, 0, side, side), info.original, from); + } else { + p.setPen(Qt::NoPen); + p.setBrush(QColor(0, 0, 0)); + p.drawEllipse(0, 0, side, side); + } + _descriptor.track->markFrameShown(); + }, raw->lifetime()); + + _descriptor.track->renderNextFrame() | rpl::start_with_next([=] { + const auto info = _descriptor.track->frameWithInfo(true); + if (!info.original.isNull() && _lastAddedIndex != info.index) { + _lastAddedIndex = info.index; + const auto ts = info.mcstimestamp; + _private.with([copy = info.original, ts](Private &that) { + that.push(ts, copy); + }); + } + raw->update(); + }, raw->lifetime()); + + raw->show(); + raw->raise(); +} + +void RoundVideoRecorder::setPaused(bool paused) { + if (_paused == paused) { + return; + } + _paused = paused; + _preview->update(); +} + + +} // namespace Ui \ No newline at end of file diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.h b/Telegram/SourceFiles/ui/controls/round_video_recorder.h new file mode 100644 index 000000000..1cfb2d289 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.h @@ -0,0 +1,65 @@ +/* +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 <crl/crl_object_on_queue.h> + +namespace Media::Capture { +struct Chunk; +} // namespace Media::Capture + +namespace tgcalls { +class VideoCaptureInterface; +} // namespace tgcalls + +namespace Webrtc { +class VideoTrack; +} // namespace Webrtc + +namespace Ui { + +class RpWidget; +class RoundVideoRecorder; + +struct RoundVideoRecorderDescriptor { + not_null<RpWidget*> container; + Fn<void(not_null<RoundVideoRecorder*>)> hidden; + std::shared_ptr<tgcalls::VideoCaptureInterface> capturer; + std::shared_ptr<Webrtc::VideoTrack> track; +}; + +struct RoundVideoResult { + QByteArray content; + QByteArray waveform; + crl::time duration = 0; +}; + +class RoundVideoRecorder final { +public: + explicit RoundVideoRecorder(RoundVideoRecorderDescriptor &&descriptor); + ~RoundVideoRecorder(); + + [[nodiscard]] Fn<void(Media::Capture::Chunk)> audioChunkProcessor(); + + void setPaused(bool paused); + void hide(Fn<void(RoundVideoResult)> done = nullptr); + +private: + class Private; + + void setup(); + + const RoundVideoRecorderDescriptor _descriptor; + std::unique_ptr<RpWidget> _preview; + crl::object_on_queue<Private> _private; + int _lastAddedIndex = 0; + bool _paused = false; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 7fd873fe6..e42ee37a2 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -366,6 +366,8 @@ PRIVATE ui/controls/invite_link_label.h ui/controls/peer_list_dummy.cpp ui/controls/peer_list_dummy.h + ui/controls/round_video_recorder.cpp + ui/controls/round_video_recorder.h ui/controls/send_as_button.cpp ui/controls/send_as_button.h ui/controls/send_button.cpp @@ -500,4 +502,6 @@ PRIVATE desktop-app::lib_spellcheck desktop-app::lib_stripe desktop-app::external_kcoreaddons + desktop-app::external_openh264 + desktop-app::external_webrtc ) diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc index 8751e27d5..fc726486e 160000 --- a/Telegram/lib_webrtc +++ b/Telegram/lib_webrtc @@ -1 +1 @@ -Subproject commit 8751e27d50d2f26b5d20673e5ddba38e90953570 +Subproject commit fc726486ebd261283583b5cd5f6a97a18b2ab6ca