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_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_listen_cancel_sure" = "Do you want to discard your recorded voice message?";
"lng_record_lock_discard" = "Discard"; "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_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_record_once_active_tooltip" = "The recipient will be able to listen only once.";
"lng_will_be_notified" = "Subscribers will be notified when you post."; "lng_will_be_notified" = "Subscribers will be notified when you post.";

View file

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

View file

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

View file

@ -628,6 +628,13 @@ public:
return _floatPlayerCorner; return _floatPlayerCorner;
} }
[[nodiscard]] bool recordVideoMessages() const {
return _recordVideoMessages;
}
void setRecordVideoMessages(bool value) {
_recordVideoMessages = value;
}
void updateDialogsWidthRatio(float64 ratio, bool nochat); void updateDialogsWidthRatio(float64 ratio, bool nochat);
[[nodiscard]] float64 dialogsWidthRatio(bool nochat) const; [[nodiscard]] float64 dialogsWidthRatio(bool nochat) const;
@ -1069,6 +1076,8 @@ private:
bool _rememberedFlashBounceNotifyFromTray = false; bool _rememberedFlashBounceNotifyFromTray = false;
bool _dialogsWidthSetToZeroWithoutChat = false; bool _dialogsWidthSetToZeroWithoutChat = false;
bool _recordVideoMessages = false;
QByteArray _photoEditorBrush; 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.h"
#include "main/main_session_settings.h" #include "main/main_session_settings.h"
#include "main/session/send_as_peers.h" #include "main/session/send_as_peers.h"
#include "webrtc/webrtc_environment.h"
#include "window/notifications_manager.h" #include "window/notifications_manager.h"
#include "window/window_adaptive.h" #include "window/window_adaptive.h"
#include "window/window_controller.h" #include "window/window_controller.h"
@ -1068,7 +1069,17 @@ void HistoryWidget::initVoiceRecordBar() {
_voiceRecordBar->recordingTipRequests( _voiceRecordBar->recordingTipRequests(
) | rpl::start_with_next([=] { ) | 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()); }, lifetime());
_voiceRecordBar->recordingStateChanges( _voiceRecordBar->recordingStateChanges(
@ -2105,6 +2116,7 @@ void HistoryWidget::showHistory(
MsgId showAtMsgId, MsgId showAtMsgId,
const TextWithEntities &highlightPart, const TextWithEntities &highlightPart,
int highlightPartOffsetHint) { int highlightPartOffsetHint) {
_pinnedClickedId = FullMsgId(); _pinnedClickedId = FullMsgId();
_minPinnedId = std::nullopt; _minPinnedId = std::nullopt;
_showAtMsgHighlightPart = {}; _showAtMsgHighlightPart = {};
@ -2299,6 +2311,8 @@ void HistoryWidget::showHistory(
_contactStatus = nullptr; _contactStatus = nullptr;
_businessBotStatus = nullptr; _businessBotStatus = nullptr;
updateRecordMediaState();
if (peerId) { if (peerId) {
using namespace HistoryView; using namespace HistoryView;
_peer = session().data().peer(peerId); _peer = session().data().peer(peerId);
@ -4254,7 +4268,10 @@ auto HistoryWidget::computeSendButtonType() const {
} else if (_isInlineBot) { } else if (_isInlineBot) {
return Type::Cancel; return Type::Cancel;
} else if (showRecordButton()) { } else if (showRecordButton()) {
return Type::Record; return (Core::App().settings().recordVideoMessages()
&& _canRecordVideoMessage)
? Type::Round
: Type::Record;
} }
return Type::Send; return Type::Send;
} }
@ -4588,7 +4605,8 @@ void HistoryWidget::sendButtonClicked() {
const auto type = _send->type(); const auto type = _send->type();
if (type == Ui::SendButton::Type::Cancel) { if (type == Ui::SendButton::Type::Cancel) {
cancelInlineBot(); cancelInlineBot();
} else if (type != Ui::SendButton::Type::Record) { } else if (type != Ui::SendButton::Type::Record
&& type != Ui::SendButton::Type::Round) {
send({}); send({});
} }
} }
@ -4878,7 +4896,7 @@ bool HistoryWidget::isSearching() const {
} }
bool HistoryWidget::showRecordButton() const { bool HistoryWidget::showRecordButton() const {
return Media::Capture::instance()->available() return _canRecordAudioMessage
&& !_voiceRecordBar->isListenState() && !_voiceRecordBar->isListenState()
&& !_voiceRecordBar->isRecordingByAnotherBar() && !_voiceRecordBar->isRecordingByAnotherBar()
&& !HasSendText(_field) && !HasSendText(_field)
@ -4909,7 +4927,9 @@ void HistoryWidget::updateSendButtonType() {
}(); }();
_send->setSlowmodeDelay(delay); _send->setSlowmodeDelay(delay);
_send->setDisabled(disabledBySlowmode _send->setDisabled(disabledBySlowmode
&& (type == Type::Send || type == Type::Record)); && (type == Type::Send
|| type == Type::Record
|| type == Type::Round));
if (delay != 0) { if (delay != 0) {
base::call_delayed( 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() { void HistoryWidget::fieldResized() {
moveFieldControls(); moveFieldControls();
updateHistoryGeometry(); updateHistoryGeometry();

View file

@ -497,6 +497,7 @@ private:
bool replyToPreviousMessage(); bool replyToPreviousMessage();
bool replyToNextMessage(); bool replyToNextMessage();
[[nodiscard]] bool showSlowmodeError(); [[nodiscard]] bool showSlowmodeError();
void updateRecordMediaState();
void hideChildWidgets(); void hideChildWidgets();
void hideSelectorControlsAnimated(); void hideSelectorControlsAnimated();
@ -746,6 +747,9 @@ private:
mtpRequestId _inlineBotResolveRequestId = 0; mtpRequestId _inlineBotResolveRequestId = 0;
bool _isInlineBot = false; bool _isInlineBot = false;
bool _canRecordVideoMessage = false;
bool _canRecordAudioMessage = false;
std::unique_ptr<HistoryView::ContactStatus> _contactStatus; std::unique_ptr<HistoryView::ContactStatus> _contactStatus;
std::unique_ptr<HistoryView::BusinessBotStatus> _businessBotStatus; 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/controls/silent_toggle.h"
#include "ui/chat/choose_send_as.h" #include "ui/chat/choose_send_as.h"
#include "ui/effects/spoiler_mess.h" #include "ui/effects/spoiler_mess.h"
#include "webrtc/webrtc_environment.h"
#include "window/window_adaptive.h" #include "window/window_adaptive.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "mainwindow.h" #include "mainwindow.h"
@ -1517,7 +1518,7 @@ void ComposeControls::orderControls() {
} }
bool ComposeControls::showRecordButton() const { bool ComposeControls::showRecordButton() const {
return ::Media::Capture::instance()->available() return _canRecordAudioMessage
&& !_voiceRecordBar->isListenState() && !_voiceRecordBar->isListenState()
&& !_voiceRecordBar->isRecordingByAnotherBar() && !_voiceRecordBar->isRecordingByAnotherBar()
&& !HasSendText(_field) && !HasSendText(_field)
@ -2413,12 +2414,36 @@ void ComposeControls::initVoiceRecordBar() {
return false; 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( _voiceRecordBar->updateSendButtonTypeRequests(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
updateSendButtonType(); updateSendButtonType();
}, _wrap->lifetime()); }, _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() { void ComposeControls::updateWrappingVisibility() {
const auto hidden = _hidden.current(); const auto hidden = _hidden.current();
const auto &restriction = _writeRestriction.current(); const auto &restriction = _writeRestriction.current();
@ -2454,7 +2479,10 @@ auto ComposeControls::computeSendButtonType() const {
} else if (_isInlineBot) { } else if (_isInlineBot) {
return Type::Cancel; return Type::Cancel;
} else if (showRecordButton()) { } else if (showRecordButton()) {
return Type::Record; return (Core::App().settings().recordVideoMessages()
&& _canRecordVideoMessage)
? Type::Round
: Type::Record;
} }
return (_mode == Mode::Normal) ? Type::Send : Type::Schedule; return (_mode == Mode::Normal) ? Type::Send : Type::Schedule;
} }
@ -2487,7 +2515,9 @@ void ComposeControls::updateSendButtonType() {
}(); }();
_send->setSlowmodeDelay(delay); _send->setSlowmodeDelay(delay);
_send->setDisabled(_sendDisabledBySlowmode.current() _send->setDisabled(_sendDisabledBySlowmode.current()
&& (type == Type::Send || type == Type::Record)); && (type == Type::Send
|| type == Type::Record
|| type == Type::Round));
} }
void ComposeControls::finishAnimating() { void ComposeControls::finishAnimating() {
@ -3149,8 +3179,9 @@ bool ComposeControls::isRecording() const {
bool ComposeControls::isRecordingPressed() const { bool ComposeControls::isRecordingPressed() const {
return !_voiceRecordBar->isRecordingLocked() return !_voiceRecordBar->isRecordingLocked()
&& (!_voiceRecordBar->isHidden() && (!_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 { rpl::producer<bool> ComposeControls::recordingActiveValue() const {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -86,6 +86,7 @@ void SendButton::paintEvent(QPaintEvent *e) {
} }
switch (_type) { switch (_type) {
case Type::Record: paintRecord(p, over); break; case Type::Record: paintRecord(p, over); break;
case Type::Round: paintRound(p, over); break;
case Type::Save: paintSave(p, over); break; case Type::Save: paintSave(p, over); break;
case Type::Cancel: paintCancel(p, over); break; case Type::Cancel: paintCancel(p, over); break;
case Type::Send: paintSend(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()); 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) { void SendButton::paintSave(QPainter &p, bool over) {
const auto &saveIcon = over const auto &saveIcon = over
? st::historyEditSaveIconOver ? st::historyEditSaveIconOver

View file

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