mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-14 13:17:08 +02:00
PoC video messages sending.
This commit is contained in:
parent
4dc7fd8cd1
commit
552343fa37
22 changed files with 1278 additions and 209 deletions
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -317,6 +317,7 @@ public:
|
|||
QByteArray result,
|
||||
VoiceWaveform waveform,
|
||||
crl::time duration,
|
||||
bool video,
|
||||
const SendAction &action);
|
||||
void sendFiles(
|
||||
Ui::PreparedList &&list,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1042,6 +1042,7 @@ void HistoryWidget::initVoiceRecordBar() {
|
|||
data.bytes,
|
||||
data.waveform,
|
||||
data.duration,
|
||||
data.video,
|
||||
action);
|
||||
_voiceRecordBar->clearListenState();
|
||||
}, lifetime());
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1224,6 +1224,7 @@ void RepliesWidget::sendVoice(ComposeControls::VoiceToSend &&data) {
|
|||
data.bytes,
|
||||
data.waveform,
|
||||
data.duration,
|
||||
data.video,
|
||||
std::move(action));
|
||||
|
||||
_composeControls->cancelReplyMessage();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -244,6 +244,7 @@ void ReplyArea::sendVoice(VoiceToSend &&data) {
|
|||
data.bytes,
|
||||
data.waveform,
|
||||
data.duration,
|
||||
data.video,
|
||||
std::move(action));
|
||||
|
||||
_controls->clearListenState();
|
||||
|
|
|
@ -1200,6 +1200,7 @@ void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) {
|
|||
data.bytes,
|
||||
data.waveform,
|
||||
data.duration,
|
||||
data.video,
|
||||
std::move(action));
|
||||
|
||||
_composeControls->cancelReplyMessage();
|
||||
|
|
|
@ -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)));
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
649
Telegram/SourceFiles/ui/controls/round_video_recorder.cpp
Normal file
649
Telegram/SourceFiles/ui/controls/round_video_recorder.cpp
Normal file
|
@ -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
|
65
Telegram/SourceFiles/ui/controls/round_video_recorder.h
Normal file
65
Telegram/SourceFiles/ui/controls/round_video_recorder.h
Normal file
|
@ -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
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 8751e27d50d2f26b5d20673e5ddba38e90953570
|
||||
Subproject commit fc726486ebd261283583b5cd5f6a97a18b2ab6ca
|
Loading…
Add table
Reference in a new issue