Allow switching between voice/video.

This commit is contained in:
John Preston 2024-10-08 22:43:24 +04:00
parent 552343fa37
commit ff44f626ba
17 changed files with 242 additions and 52 deletions

View file

@ -3246,7 +3246,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_record_lock_cancel_sure" = "Do you want to stop recording and discard your voice message?";
"lng_record_listen_cancel_sure" = "Do you want to discard your recorded voice message?";
"lng_record_lock_discard" = "Discard";
"lng_record_hold_tip" = "Please hold the mouse button pressed to record a voice message.";
"lng_record_voice_tip" = "Hold to record audio. Click to switch to video.";
"lng_record_video_tip" = "Hold to record video. Click to switch to audio.";
"lng_record_once_first_tooltip" = "Click to set this message to **Play Once**.";
"lng_record_once_active_tooltip" = "The recipient will be able to listen only once.";
"lng_will_be_notified" = "Subscribers will be notified when you post.";

View file

@ -150,6 +150,8 @@ SendButton {
inner: IconButton;
record: icon;
recordOver: icon;
round: icon;
roundOver: icon;
sendDisabledFg: color;
}
@ -1159,6 +1161,10 @@ historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive
historyRecordVoiceOnceFgOver: icon {{ "voice_lock/audio_once_number", windowFgActive }};
historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }};
historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }};
historyRecordRound: icon {{ "info/info_media_round", historyRecordVoiceFg }};
historyRecordRoundOver: icon {{ "info/info_media_round", historyRecordVoiceFgOver }};
historyRecordRoundActive: icon {{ "info/info_media_round", historyRecordVoiceFgActiveIcon }};
historyRecordRoundIconPosition: point(0px, 0px);
historyRecordSendIconPosition: point(2px, 0px);
historyRecordVoiceRippleBgActive: lightButtonBgOver;
historyRecordSignalRadius: 5px;
@ -1264,6 +1270,8 @@ historySend: SendButton {
}
record: historyRecordVoice;
recordOver: historyRecordVoiceOver;
round: historyRecordRound;
roundOver: historyRecordRoundOver;
sendDisabledFg: historyComposeIconFg;
}

View file

@ -222,7 +222,7 @@ QByteArray Settings::serialize() const {
+ Serialize::stringSize(_customFontFamily)
+ sizeof(qint32) * 3
+ Serialize::bytearraySize(_tonsiteStorageToken)
+ sizeof(qint32) * 3;
+ sizeof(qint32) * 4;
auto result = QByteArray();
result.reserve(size);
@ -379,7 +379,8 @@ QByteArray Settings::serialize() const {
<< _tonsiteStorageToken
<< qint32(_includeMutedCounterFolders ? 1 : 0)
<< qint32(_ivZoom.current())
<< qint32(_skipToastsInFocus ? 1 : 0);
<< qint32(_skipToastsInFocus ? 1 : 0)
<< qint32(_recordVideoMessages ? 1 : 0);
}
Ensures(result.size() == size);
@ -505,6 +506,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
QByteArray tonsiteStorageToken = _tonsiteStorageToken;
qint32 ivZoom = _ivZoom.current();
qint32 skipToastsInFocus = _skipToastsInFocus ? 1 : 0;
qint32 recordVideoMessages = _recordVideoMessages ? 1 : 0;
stream >> themesAccentColors;
if (!stream.atEnd()) {
@ -820,6 +822,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
if (!stream.atEnd()) {
stream >> skipToastsInFocus;
}
if (!stream.atEnd()) {
stream >> recordVideoMessages;
}
if (stream.status() != QDataStream::Ok) {
LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()"));
@ -1033,6 +1038,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
_tonsiteStorageToken = tonsiteStorageToken;
_ivZoom = ivZoom;
_skipToastsInFocus = (skipToastsInFocus == 1);
_recordVideoMessages = (recordVideoMessages == 1);
}
QString Settings::getSoundPath(const QString &key) const {
@ -1422,6 +1428,7 @@ void Settings::resetOnLastLogout() {
_storiesClickTooltipHidden = false;
_ttlVoiceClickTooltipHidden = false;
_ivZoom = 100;
_recordVideoMessages = false;
_recentEmojiPreload.clear();
_recentEmoji.clear();

View file

@ -628,6 +628,13 @@ public:
return _floatPlayerCorner;
}
[[nodiscard]] bool recordVideoMessages() const {
return _recordVideoMessages;
}
void setRecordVideoMessages(bool value) {
_recordVideoMessages = value;
}
void updateDialogsWidthRatio(float64 ratio, bool nochat);
[[nodiscard]] float64 dialogsWidthRatio(bool nochat) const;
@ -1069,6 +1076,8 @@ private:
bool _rememberedFlashBounceNotifyFromTray = false;
bool _dialogsWidthSetToZeroWithoutChat = false;
bool _recordVideoMessages = false;
QByteArray _photoEditorBrush;
};

View file

@ -150,6 +150,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "main/session/send_as_peers.h"
#include "webrtc/webrtc_environment.h"
#include "window/notifications_manager.h"
#include "window/window_adaptive.h"
#include "window/window_controller.h"
@ -1068,7 +1069,17 @@ void HistoryWidget::initVoiceRecordBar() {
_voiceRecordBar->recordingTipRequests(
) | rpl::start_with_next([=] {
controller()->showToast(tr::lng_record_hold_tip(tr::now));
Core::App().settings().setRecordVideoMessages(
!Core::App().settings().recordVideoMessages());
updateSendButtonType();
switch (_send->type()) {
case Ui::SendButton::Type::Record:
controller()->showToast(tr::lng_record_voice_tip(tr::now));
break;
case Ui::SendButton::Type::Round:
controller()->showToast(tr::lng_record_video_tip(tr::now));
break;
}
}, lifetime());
_voiceRecordBar->recordingStateChanges(
@ -2105,6 +2116,7 @@ void HistoryWidget::showHistory(
MsgId showAtMsgId,
const TextWithEntities &highlightPart,
int highlightPartOffsetHint) {
_pinnedClickedId = FullMsgId();
_minPinnedId = std::nullopt;
_showAtMsgHighlightPart = {};
@ -2299,6 +2311,8 @@ void HistoryWidget::showHistory(
_contactStatus = nullptr;
_businessBotStatus = nullptr;
updateRecordMediaState();
if (peerId) {
using namespace HistoryView;
_peer = session().data().peer(peerId);
@ -4254,7 +4268,10 @@ auto HistoryWidget::computeSendButtonType() const {
} else if (_isInlineBot) {
return Type::Cancel;
} else if (showRecordButton()) {
return Type::Record;
return (Core::App().settings().recordVideoMessages()
&& _canRecordVideoMessage)
? Type::Round
: Type::Record;
}
return Type::Send;
}
@ -4588,7 +4605,8 @@ void HistoryWidget::sendButtonClicked() {
const auto type = _send->type();
if (type == Ui::SendButton::Type::Cancel) {
cancelInlineBot();
} else if (type != Ui::SendButton::Type::Record) {
} else if (type != Ui::SendButton::Type::Record
&& type != Ui::SendButton::Type::Round) {
send({});
}
}
@ -4878,7 +4896,7 @@ bool HistoryWidget::isSearching() const {
}
bool HistoryWidget::showRecordButton() const {
return Media::Capture::instance()->available()
return _canRecordAudioMessage
&& !_voiceRecordBar->isListenState()
&& !_voiceRecordBar->isRecordingByAnotherBar()
&& !HasSendText(_field)
@ -4909,7 +4927,9 @@ void HistoryWidget::updateSendButtonType() {
}();
_send->setSlowmodeDelay(delay);
_send->setDisabled(disabledBySlowmode
&& (type == Type::Send || type == Type::Record));
&& (type == Type::Send
|| type == Type::Record
|| type == Type::Round));
if (delay != 0) {
base::call_delayed(
@ -5479,6 +5499,15 @@ void HistoryWidget::inlineBotChanged() {
}
}
void HistoryWidget::updateRecordMediaState() {
Media::Capture::instance()->check();
_canRecordAudioMessage = Media::Capture::instance()->available();
const auto environment = &Core::App().mediaDevices();
const auto type = Webrtc::DeviceType::Camera;
_canRecordVideoMessage = !environment->devices(type).empty();
}
void HistoryWidget::fieldResized() {
moveFieldControls();
updateHistoryGeometry();

View file

@ -497,6 +497,7 @@ private:
bool replyToPreviousMessage();
bool replyToNextMessage();
[[nodiscard]] bool showSlowmodeError();
void updateRecordMediaState();
void hideChildWidgets();
void hideSelectorControlsAnimated();
@ -746,6 +747,9 @@ private:
mtpRequestId _inlineBotResolveRequestId = 0;
bool _isInlineBot = false;
bool _canRecordVideoMessage = false;
bool _canRecordAudioMessage = false;
std::unique_ptr<HistoryView::ContactStatus> _contactStatus;
std::unique_ptr<HistoryView::BusinessBotStatus> _businessBotStatus;

View file

@ -81,6 +81,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/controls/silent_toggle.h"
#include "ui/chat/choose_send_as.h"
#include "ui/effects/spoiler_mess.h"
#include "webrtc/webrtc_environment.h"
#include "window/window_adaptive.h"
#include "window/window_session_controller.h"
#include "mainwindow.h"
@ -1517,7 +1518,7 @@ void ComposeControls::orderControls() {
}
bool ComposeControls::showRecordButton() const {
return ::Media::Capture::instance()->available()
return _canRecordAudioMessage
&& !_voiceRecordBar->isListenState()
&& !_voiceRecordBar->isRecordingByAnotherBar()
&& !HasSendText(_field)
@ -2413,12 +2414,36 @@ void ComposeControls::initVoiceRecordBar() {
return false;
});
_voiceRecordBar->recordingTipRequests(
) | rpl::start_with_next([=] {
Core::App().settings().setRecordVideoMessages(
!Core::App().settings().recordVideoMessages());
updateSendButtonType();
switch (_send->type()) {
case Ui::SendButton::Type::Record:
_show->showToast(tr::lng_record_voice_tip(tr::now));
break;
case Ui::SendButton::Type::Round:
_show->showToast(tr::lng_record_video_tip(tr::now));
break;
}
}, _wrap->lifetime());
_voiceRecordBar->updateSendButtonTypeRequests(
) | rpl::start_with_next([=] {
updateSendButtonType();
}, _wrap->lifetime());
}
void ComposeControls::updateRecordMediaState() {
::Media::Capture::instance()->check();
_canRecordAudioMessage = ::Media::Capture::instance()->available();
const auto environment = &Core::App().mediaDevices();
const auto type = Webrtc::DeviceType::Camera;
_canRecordVideoMessage = !environment->devices(type).empty();
}
void ComposeControls::updateWrappingVisibility() {
const auto hidden = _hidden.current();
const auto &restriction = _writeRestriction.current();
@ -2454,7 +2479,10 @@ auto ComposeControls::computeSendButtonType() const {
} else if (_isInlineBot) {
return Type::Cancel;
} else if (showRecordButton()) {
return Type::Record;
return (Core::App().settings().recordVideoMessages()
&& _canRecordVideoMessage)
? Type::Round
: Type::Record;
}
return (_mode == Mode::Normal) ? Type::Send : Type::Schedule;
}
@ -2487,7 +2515,9 @@ void ComposeControls::updateSendButtonType() {
}();
_send->setSlowmodeDelay(delay);
_send->setDisabled(_sendDisabledBySlowmode.current()
&& (type == Type::Send || type == Type::Record));
&& (type == Type::Send
|| type == Type::Record
|| type == Type::Round));
}
void ComposeControls::finishAnimating() {
@ -3149,8 +3179,9 @@ bool ComposeControls::isRecording() const {
bool ComposeControls::isRecordingPressed() const {
return !_voiceRecordBar->isRecordingLocked()
&& (!_voiceRecordBar->isHidden()
|| (_send->type() == Ui::SendButton::Type::Record
&& _send->isDown()));
|| (_send->isDown()
&& (_send->type() == Ui::SendButton::Type::Record
|| _send->type() == Ui::SendButton::Type::Round)));
}
rpl::producer<bool> ComposeControls::recordingActiveValue() const {

View file

@ -278,6 +278,7 @@ private:
bool updateSendAsButton();
void updateAttachBotsMenu();
void updateHeight();
void updateRecordMediaState();
void updateWrappingVisibility();
void updateControlsVisibility();
void updateControlsGeometry(QSize size);
@ -437,6 +438,9 @@ private:
bool _botCommandShown = false;
bool _likeShown = false;
bool _canRecordVideoMessage = false;
bool _canRecordAudioMessage = false;
FullMsgId _editingId;
std::shared_ptr<Data::PhotoMedia> _photoEditMedia;
bool _canReplaceMedia = false;

View file

@ -1535,6 +1535,7 @@ void VoiceRecordBar::init() {
return;
}
_recordingTipRequired = true;
_recordingVideo = (_send->type() == Ui::SendButton::Type::Round);
_startTimer.callOnce(st::universalDuration);
} else if (e->type() == QEvent::MouseButtonRelease) {
if (base::take(_recordingTipRequired)) {
@ -1589,6 +1590,11 @@ void VoiceRecordBar::visibilityAnimate(bool show, Fn<void()> &&callback) {
// _videoHiding.back()->hide();
//}
AssertIsDebug();
if (_send->type() == Ui::SendButton::Type::Round) {
_level->setType(VoiceRecordButton::Type::Round);
} else {
_level->setType(VoiceRecordButton::Type::Record);
}
const auto to = show ? 1. : 0.;
const auto from = show ? 0. : 1.;
auto animationCallback = [=, callback = std::move(callback)](auto value) {
@ -1656,7 +1662,6 @@ void VoiceRecordBar::startRecording() {
if (isRecording()) {
return;
}
_recordingVideo = true; AssertIsDebug();
auto appearanceCallback = [=] {
if (_showAnimation.animating()) {
return;
@ -1694,6 +1699,15 @@ void VoiceRecordBar::startRecording() {
}, [=] {
stop(false);
}, _recordingLifetime);
if (_videoRecorder) {
_videoRecorder->updated(
) | rpl::start_with_next_error([=](const Update &update) {
_recordingTipRequired = (update.samples < kMinSamples);
recordUpdated(update.level, update.samples);
}, [=] {
stop(false);
}, _recordingLifetime);
}
_recordingLifetime.add([=] {
_recording = false;
});
@ -2013,7 +2027,8 @@ bool VoiceRecordBar::isListenState() const {
}
bool VoiceRecordBar::isTypeRecord() const {
return (_send->type() == Ui::SendButton::Type::Record);
return (_send->type() == Ui::SendButton::Type::Record)
|| (_send->type() == Ui::SendButton::Type::Round);
}
bool VoiceRecordBar::isRecordingByAnotherBar() const {

View file

@ -137,10 +137,14 @@ void VoiceRecordButton::init() {
const auto state = *currentState;
const auto icon = (state == Type::Send)
? st::historySendIcon
: st::historyRecordVoiceActive;
: (state == Type::Record)
? st::historyRecordVoiceActive
: st::historyRecordRoundActive;
const auto position = (state == Type::Send)
? st::historyRecordSendIconPosition
: QPoint(0, 0);
: (state == Type::Record)
? QPoint(0, 0)
: st::historyRecordRoundIconPosition;
icon.paint(
p,
-icon.width() / 2 + position.x(),

View file

@ -31,6 +31,7 @@ public:
enum class Type {
Send,
Record,
Round,
};
void setType(Type state);

View file

@ -480,6 +480,8 @@ storiesLike: IconButton(storiesAttach) {
}
storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }};
storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }};
storiesRecordRound: icon {{ "info/info_media_round", storiesComposeGrayIcon }};
storiesRecordRoundOver: icon {{ "info/info_media_round", storiesComposeGrayIcon }};
storiesRemoveSet: IconButton(stickerPanRemoveSet) {
icon: icon {{ "simple_close", storiesComposeGrayIcon }};
iconOver: icon {{ "simple_close", storiesComposeGrayIcon }};
@ -686,6 +688,8 @@ storiesComposeControls: ComposeControls(defaultComposeControls) {
}
record: storiesRecordVoice;
recordOver: storiesRecordVoiceOver;
round: storiesRecordRound;
roundOver: storiesRecordRoundOver;
sendDisabledFg: storiesComposeGrayText;
}
attach: storiesAttach;

View file

@ -679,7 +679,7 @@ object_ptr<Ui::GenericBox> ChooseCameraDeviceBox(
const style::Radio *radioSt) {
return Box(
ChooseMediaDeviceBox,
tr::lng_settings_call_device_default(),
tr::lng_settings_call_camera(),
Core::App().mediaDevices().devicesValue(DeviceType::Camera),
std::move(currentId),
std::move(chosen),

View file

@ -19,7 +19,7 @@ namespace Ui {
namespace {
constexpr auto kSide = 400;
constexpr auto kOutputFilename = "C:\\Tmp\\TestVideo\\output.mp4";
constexpr auto kUpdateEach = crl::time(100);
using namespace FFmpeg;
@ -33,6 +33,9 @@ public:
void push(int64 mcstimestamp, const QImage &frame);
void push(const Media::Capture::Chunk &chunk);
using Update = Media::Capture::Update;
[[nodiscard]] rpl::producer<Update, rpl::empty_error> updated() const;
[[nodiscard]] RoundVideoResult finish();
private:
@ -42,6 +45,23 @@ private:
int write(uint8_t *buf, int buf_size);
int64_t seek(int64_t offset, int whence);
void initEncoding();
bool initVideo();
bool initAudio();
void deinitEncoding();
void finishEncoding();
void fail();
void encodeVideoFrame(int64 mcstimestamp, const QImage &frame);
void encodeAudioFrame(const Media::Capture::Chunk &chunk);
bool writeFrame(
const FramePointer &frame,
const CodecPointer &codec,
AVStream *stream);
void updateMaxLevel(const Media::Capture::Chunk &chunk);
void updateResultDuration(int64 pts, AVRational timeBase);
const crl::weak_on_queue<Private> _weak;
FormatPointer _format;
@ -72,18 +92,9 @@ private:
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);
ushort _maxLevelSinceLastUpdate = 0;
crl::time _lastUpdateDuration = 0;
rpl::event_stream<Update, rpl::empty_error> _updates;
};
@ -94,11 +105,6 @@ RoundVideoRecorder::Private::Private(crl::weak_on_queue<Private> weak)
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) {
@ -155,7 +161,7 @@ void RoundVideoRecorder::Private::initEncoding() {
"mp4"_q);
if (!initVideo() || !initAudio()) {
deinitEncoding();
fail();
return;
}
@ -164,7 +170,7 @@ void RoundVideoRecorder::Private::initEncoding() {
nullptr));
if (error) {
LogError("avformat_write_header", error);
deinitEncoding();
fail();
}
}
@ -343,6 +349,11 @@ void RoundVideoRecorder::Private::finishEncoding() {
deinitEncoding();
}
auto RoundVideoRecorder::Private::updated() const
-> rpl::producer<Update, rpl::empty_error> {
return _updates.events();
}
RoundVideoResult RoundVideoRecorder::Private::finish() {
if (!_format) {
return {};
@ -355,6 +366,11 @@ RoundVideoResult RoundVideoRecorder::Private::finish() {
};
};
void RoundVideoRecorder::Private::fail() {
deinitEncoding();
_updates.fire_error({});
}
void RoundVideoRecorder::Private::deinitEncoding() {
_swsContext = nullptr;
_videoCodec = nullptr;
@ -406,7 +422,7 @@ void RoundVideoRecorder::Private::encodeVideoFrame(
AV_PIX_FMT_YUV420P,
&_swsContext);
if (!_swsContext) {
deinitEncoding();
fail();
return;
}
@ -444,14 +460,15 @@ void RoundVideoRecorder::Private::encodeVideoFrame(
_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) {
void RoundVideoRecorder::Private::encodeAudioFrame(
const Media::Capture::Chunk &chunk) {
updateMaxLevel(chunk);
if (_audioTail.isEmpty()) {
_audioTail = chunk.samples;
} else {
@ -459,7 +476,8 @@ void RoundVideoRecorder::Private::encodeAudioFrame(const Media::Capture::Chunk &
}
const int inSamples = _audioTail.size() / sizeof(int16_t);
const uint8_t *inData = reinterpret_cast<const uint8_t*>(_audioTail.constData());
const uint8_t *inData = reinterpret_cast<const uint8_t*>(
_audioTail.constData());
int samplesProcessed = 0;
while (samplesProcessed + _audioCodec->frame_size <= inSamples) {
@ -484,7 +502,7 @@ void RoundVideoRecorder::Private::encodeAudioFrame(const Media::Capture::Chunk &
if (error) {
LogError("swr_convert", error);
deinitEncoding();
fail();
return;
}
@ -493,8 +511,6 @@ void RoundVideoRecorder::Private::encodeAudioFrame(const Media::Capture::Chunk &
_audioFrame->pts = _audioPts;
_audioPts += _audioFrame->nb_samples;
LOG(("Audio At: %1").arg(_audioFrame->pts / 48'000.));
if (!writeFrame(_audioFrame, _audioCodec, _audioStream)) {
return;
}
@ -514,10 +530,14 @@ bool RoundVideoRecorder::Private::writeFrame(
const FramePointer &frame,
const CodecPointer &codec,
AVStream *stream) {
if (frame) {
updateResultDuration(frame->pts, codec->time_base);
}
auto error = AvErrorWrap(avcodec_send_frame(codec.get(), frame.get()));
if (error) {
LogError("avcodec_send_frame", error);
deinitEncoding();
fail();
return false;
}
@ -533,21 +553,19 @@ bool RoundVideoRecorder::Private::writeFrame(
return true; // Encoding finished
} else if (error) {
LogError("avcodec_receive_packet", error);
deinitEncoding();
fail();
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));
updateResultDuration(pkt->pts, stream->time_base);
error = AvErrorWrap(av_interleaved_write_frame(_format.get(), pkt));
if (error) {
LogError("av_interleaved_write_frame", error);
deinitEncoding();
fail();
return false;
}
}
@ -555,6 +573,30 @@ bool RoundVideoRecorder::Private::writeFrame(
return true;
}
void RoundVideoRecorder::Private::updateMaxLevel(
const Media::Capture::Chunk &chunk) {
const auto &list = chunk.samples;
const auto samples = int(list.size() / sizeof(ushort));
const auto data = reinterpret_cast<const ushort*>(list.constData());
for (const auto value : gsl::make_span(data, samples)) {
accumulate_max(_maxLevelSinceLastUpdate, value);
}
}
void RoundVideoRecorder::Private::updateResultDuration(
int64 pts,
AVRational timeBase) {
accumulate_max(_resultDuration, PtsToTimeCeil(pts, timeBase));
if (_lastUpdateDuration + kUpdateEach >= _resultDuration) {
_lastUpdateDuration = _resultDuration;
_updates.fire({
.samples = int(_resultDuration * 48),
.level = base::take(_maxLevelSinceLastUpdate),
});
}
}
RoundVideoRecorder::RoundVideoRecorder(
RoundVideoRecorderDescriptor &&descriptor)
: _descriptor(std::move(descriptor))
@ -573,6 +615,13 @@ Fn<void(Media::Capture::Chunk)> RoundVideoRecorder::audioChunkProcessor() {
};
}
auto RoundVideoRecorder::updated() const
-> rpl::producer<Update, rpl::empty_error> {
return _private.producer_on_main([](const Private &that) {
return that.updated();
});
}
void RoundVideoRecorder::hide(Fn<void(RoundVideoResult)> done) {
if (done) {
_private.with([done = std::move(done)](Private &that) {
@ -642,6 +691,9 @@ void RoundVideoRecorder::setPaused(bool paused) {
return;
}
_paused = paused;
_descriptor.track->setState(paused
? Webrtc::VideoState::Inactive
: Webrtc::VideoState::Active);
_preview->update();
}

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Media::Capture {
struct Chunk;
struct Update;
} // namespace Media::Capture
namespace tgcalls {
@ -49,6 +50,9 @@ public:
void setPaused(bool paused);
void hide(Fn<void(RoundVideoResult)> done = nullptr);
using Update = Media::Capture::Update;
[[nodiscard]] rpl::producer<Update, rpl::empty_error> updated() const;
private:
class Private;

View file

@ -86,6 +86,7 @@ void SendButton::paintEvent(QPaintEvent *e) {
}
switch (_type) {
case Type::Record: paintRecord(p, over); break;
case Type::Round: paintRound(p, over); break;
case Type::Save: paintSave(p, over); break;
case Type::Cancel: paintCancel(p, over); break;
case Type::Send: paintSend(p, over); break;
@ -108,6 +109,20 @@ void SendButton::paintRecord(QPainter &p, bool over) {
icon.paintInCenter(p, rect());
}
void SendButton::paintRound(QPainter &p, bool over) {
if (!isDisabled()) {
paintRipple(
p,
(width() - _st.inner.rippleAreaSize) / 2,
_st.inner.rippleAreaPosition.y());
}
const auto &icon = (isDisabled() || !over)
? _st.round
: _st.roundOver;
icon.paintInCenter(p, rect());
}
void SendButton::paintSave(QPainter &p, bool over) {
const auto &saveIcon = over
? st::historyEditSaveIconOver

View file

@ -26,6 +26,7 @@ public:
Schedule,
Save,
Record,
Round,
Cancel,
Slowmode,
};
@ -47,6 +48,7 @@ private:
[[nodiscard]] bool isSlowmode() const;
void paintRecord(QPainter &p, bool over);
void paintRound(QPainter &p, bool over);
void paintSave(QPainter &p, bool over);
void paintCancel(QPainter &p, bool over);
void paintSend(QPainter &p, bool over);