Show mini-thumbnails when pausing recording.

This commit is contained in:
John Preston 2024-10-22 09:49:33 +04:00
parent c8d4818d22
commit 9514b6eecd
4 changed files with 178 additions and 42 deletions

View file

@ -203,6 +203,44 @@ void PaintWaveform(
} }
} }
void FillWithMinithumbs(
QPainter &p,
not_null<const Ui::RoundVideoResult*> data,
QRect rect,
float64 progress) {
if (!data->minithumbsCount || !data->minithumbSize || rect.isEmpty()) {
return;
}
const auto size = rect.height();
const auto single = data->minithumbSize;
const auto perrow = data->minithumbs.width() / single;
const auto thumbs = (rect.width() + size - 1) / size;
if (!thumbs || !perrow) {
return;
}
for (auto i = 0; i != thumbs - 1; ++i) {
const auto index = (i * data->minithumbsCount) / thumbs;
p.drawImage(
QRect(rect.x() + i * size, rect.y(), size, size),
data->minithumbs,
QRect(
(index % perrow) * single,
(index / perrow) * single,
single,
single));
}
const auto last = rect.width() - (thumbs - 1) * size;
const auto index = ((thumbs - 1) * data->minithumbsCount) / thumbs;
p.drawImage(
QRect(rect.x() + (thumbs - 1) * size, rect.y(), last, size),
data->minithumbs,
QRect(
(index % perrow) * single,
(index / perrow) * single,
(last * single) / size,
single));
}
[[nodiscard]] QRect DrawLockCircle( [[nodiscard]] QRect DrawLockCircle(
QPainter &p, QPainter &p,
const QRect &widgetRect, const QRect &widgetRect,
@ -428,7 +466,7 @@ public:
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
const style::RecordBar &st, const style::RecordBar &st,
not_null<Main::Session*> session, not_null<Main::Session*> session,
::Media::Capture::Result *data, not_null<Ui::RoundVideoResult*> data,
const style::font &font); const style::font &font);
void requestPaintProgress(float64 progress); void requestPaintProgress(float64 progress);
@ -456,7 +494,7 @@ private:
const not_null<DocumentData*> _document; const not_null<DocumentData*> _document;
const std::unique_ptr<VoiceData> _voiceData; const std::unique_ptr<VoiceData> _voiceData;
const std::shared_ptr<Data::DocumentMedia> _mediaView; const std::shared_ptr<Data::DocumentMedia> _mediaView;
const not_null<::Media::Capture::Result*> _data; const not_null<Ui::RoundVideoResult*> _data;
const base::unique_qptr<Ui::IconButton> _delete; const base::unique_qptr<Ui::IconButton> _delete;
const style::font &_durationFont; const style::font &_durationFont;
const QString _duration; const QString _duration;
@ -486,7 +524,7 @@ ListenWrap::ListenWrap(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
const style::RecordBar &st, const style::RecordBar &st,
not_null<Main::Session*> session, not_null<Main::Session*> session,
::Media::Capture::Result *data, not_null<Ui::RoundVideoResult*> data,
const style::font &font) const style::font &font)
: _parent(parent) : _parent(parent)
, _st(st) , _st(st)
@ -604,20 +642,27 @@ void ListenWrap::init() {
} }
// Waveform paint. // Waveform paint.
{ const auto waveformRect = (progress == 1.)
const auto rect = (progress == 1.) ? _waveformFgRect
? _waveformFgRect : computeWaveformRect(bgCenterRect);
: computeWaveformRect(bgCenterRect); if (!waveformRect.isEmpty()) {
if (rect.width() > 0) { const auto playProgress = _playProgress.current();
p.translate(rect.topLeft()); if (_data->minithumbs.isNull()) {
p.translate(waveformRect.topLeft());
PaintWaveform( PaintWaveform(
p, p,
_voiceData.get(), _voiceData.get(),
rect.width(), waveformRect.width(),
_activeWaveformBar, _activeWaveformBar,
_inactiveWaveformBar, _inactiveWaveformBar,
_playProgress.current()); playProgress);
p.resetTransform(); p.resetTransform();
} else {
FillWithMinithumbs(
p,
_data,
waveformRect,
playProgress);
} }
} }
} }
@ -631,9 +676,11 @@ void ListenWrap::initPlayButton() {
using namespace ::Media::Player; using namespace ::Media::Player;
using State = TrackState; using State = TrackState;
_mediaView->setBytes(_data->bytes); _mediaView->setBytes(_data->content);
_document->size = _data->bytes.size(); _document->size = _data->content.size();
_document->type = _data->video ? RoundVideoDocument : VoiceDocument; _document->type = _data->minithumbs.isNull()
? VoiceDocument
: RoundVideoDocument;
const auto &play = _playPauseSt.playOuter; const auto &play = _playPauseSt.playOuter;
const auto &width = _waveformBgFinalCenterRect.height(); const auto &width = _waveformBgFinalCenterRect.height();
@ -1688,10 +1735,7 @@ void VoiceRecordBar::startRecording() {
instance()->pause(false, nullptr); instance()->pause(false, nullptr);
if (_videoRecorder) { if (_videoRecorder) {
_videoRecorder->resume({ _videoRecorder->resume({
.video = { .video = std::move(_data),
.content = _data.bytes,
.duration = _data.duration,
},
}); });
} }
} else { } else {
@ -1836,12 +1880,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
window()->activateWindow(); window()->activateWindow();
_paused = true; _paused = true;
_data = ::Media::Capture::Result{ _data = std::move(data);
.bytes = std::move(data.content),
//.waveform = std::move(data.waveform),
.duration = data.duration,
.video = true,
};
_listen = std::make_unique<ListenWrap>( _listen = std::make_unique<ListenWrap>(
this, this,
_st, _st,
@ -1861,7 +1900,11 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
return; return;
} }
_paused = true; _paused = true;
_data = std::move(data); _data = Ui::RoundVideoResult{
.content = std::move(data.bytes),
.waveform = std::move(data.waveform),
.duration = data.duration,
};
window()->raise(); window()->raise();
window()->activateWindow(); window()->activateWindow();
@ -1906,7 +1949,11 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
stop(false); stop(false);
return; return;
} }
_data = std::move(data); _data = Ui::RoundVideoResult{
.content = std::move(data.bytes),
.waveform = std::move(data.waveform),
.duration = data.duration,
};
window()->raise(); window()->raise();
window()->activateWindow(); window()->activateWindow();
@ -1916,7 +1963,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
: 0), : 0),
}; };
_sendVoiceRequests.fire({ _sendVoiceRequests.fire({
_data.bytes, _data.content,
_data.waveform, _data.waveform,
_data.duration, _data.duration,
options, options,
@ -1983,7 +2030,7 @@ void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {
options.ttlSeconds = std::numeric_limits<int>::max(); options.ttlSeconds = std::numeric_limits<int>::max();
} }
_sendVoiceRequests.fire({ _sendVoiceRequests.fire({
_data.bytes, _data.content,
_data.waveform, _data.waveform,
_data.duration, _data.duration,
options, options,

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/timer.h" #include "base/timer.h"
#include "history/view/controls/compose_controls_common.h" #include "history/view/controls/compose_controls_common.h"
#include "media/audio/media_audio_capture_common.h" #include "media/audio/media_audio_capture_common.h"
#include "ui/controls/round_video_recorder.h"
#include "ui/effects/animations.h" #include "ui/effects/animations.h"
#include "ui/round_rect.h" #include "ui/round_rect.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
@ -170,7 +171,7 @@ private:
std::unique_ptr<Ui::AbstractButton> _ttlButton; std::unique_ptr<Ui::AbstractButton> _ttlButton;
std::unique_ptr<ListenWrap> _listen; std::unique_ptr<ListenWrap> _listen;
::Media::Capture::Result _data; Ui::RoundVideoResult _data;
rpl::variable<bool> _paused; rpl::variable<bool> _paused;
base::Timer _startTimer; base::Timer _startTimer;

View file

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
#include "webrtc/webrtc_video_track.h" #include "webrtc/webrtc_video_track.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
namespace Ui { namespace Ui {
@ -30,6 +31,8 @@ constexpr auto kMinDuration = crl::time(200);
constexpr auto kMaxDuration = 60 * crl::time(1000); constexpr auto kMaxDuration = 60 * crl::time(1000);
constexpr auto kInitTimeout = 5 * crl::time(1000); constexpr auto kInitTimeout = 5 * crl::time(1000);
constexpr auto kBlurredSize = 64; constexpr auto kBlurredSize = 64;
constexpr auto kMinithumbsPerSecond = 5;
constexpr auto kMinithumbsInRow = 16;
using namespace FFmpeg; using namespace FFmpeg;
@ -66,11 +69,19 @@ struct ReadBytesWrap {
}; };
}; };
[[nodiscard]] int MinithumbSize() {
const auto full = st::historySendSize.height();
const auto margin = st::historyRecordWaveformBgMargins;
const auto outer = full - margin.top() - margin.bottom();
const auto inner = outer - 2 * st::msgWaveformMin;
return inner * style::DevicePixelRatio();
}
} // namespace } // namespace
class RoundVideoRecorder::Private final { class RoundVideoRecorder::Private final {
public: public:
Private(crl::weak_on_queue<Private> weak); Private(crl::weak_on_queue<Private> weak, int minithumbSize);
~Private(); ~Private();
void push(int64 mcstimestamp, const QImage &frame); void push(int64 mcstimestamp, const QImage &frame);
@ -103,6 +114,13 @@ private:
int64_t seek(int64_t offset, int whence); int64_t seek(int64_t offset, int whence);
void initEncoding(); void initEncoding();
void initCircleMask();
void initMinithumbsCanvas();
void maybeSaveMinithumb(
not_null<AVFrame*> frame,
const QImage &original,
QRect crop);
bool initVideo(); bool initVideo();
bool initAudio(); bool initAudio();
void notifyFinished(); void notifyFinished();
@ -122,7 +140,6 @@ private:
void updateResultDuration(int64 pts, AVRational timeBase); void updateResultDuration(int64 pts, AVRational timeBase);
void cutCircleFromYUV420P(not_null<AVFrame*> frame); void cutCircleFromYUV420P(not_null<AVFrame*> frame);
void initCircleMask();
[[nodiscard]] RoundVideoResult appendToPrevious(RoundVideoResult video); [[nodiscard]] RoundVideoResult appendToPrevious(RoundVideoResult video);
[[nodiscard]] static FormatPointer OpenInputContext( [[nodiscard]] static FormatPointer OpenInputContext(
@ -169,6 +186,11 @@ private:
crl::time _lastUpdateDuration = 0; crl::time _lastUpdateDuration = 0;
rpl::event_stream<Update, Error> _updates; rpl::event_stream<Update, Error> _updates;
crl::time _minithumbNextTimestamp = 0;
const int _minithumbSize = 0;
int _minithumbsCount = 0;
QImage _minithumbs;
crl::time _maxDuration = 0; crl::time _maxDuration = 0;
RoundVideoResult _previous; RoundVideoResult _previous;
@ -185,12 +207,16 @@ RoundVideoRecorder::Private::CopyContext::CopyContext() {
ranges::fill(lastDts, std::numeric_limits<int64>::min()); ranges::fill(lastDts, std::numeric_limits<int64>::min());
} }
RoundVideoRecorder::Private::Private(crl::weak_on_queue<Private> weak) RoundVideoRecorder::Private::Private(
crl::weak_on_queue<Private> weak,
int minithumbSize)
: _weak(std::move(weak)) : _weak(std::move(weak))
, _minithumbSize(minithumbSize)
, _maxDuration(kMaxDuration) , _maxDuration(kMaxDuration)
, _timeoutTimer(_weak, [=] { timeout(); }) { , _timeoutTimer(_weak, [=] { timeout(); }) {
initEncoding(); initEncoding();
initCircleMask(); initCircleMask();
initMinithumbsCanvas();
_timeoutTimer.callOnce(kInitTimeout); _timeoutTimer.callOnce(kInitTimeout);
} }
@ -452,8 +478,11 @@ RoundVideoResult RoundVideoRecorder::Private::finish() {
finishEncoding(); finishEncoding();
auto result = appendToPrevious({ auto result = appendToPrevious({
.content = base::take(_result), .content = base::take(_result),
.waveform = QByteArray(),
.duration = base::take(_resultDuration), .duration = base::take(_resultDuration),
//.waveform = {},
.minithumbs = base::take(_minithumbs),
.minithumbsCount = base::take(_minithumbsCount),
.minithumbSize = _minithumbSize,
}); });
if (result.duration < kMinDuration) { if (result.duration < kMinDuration) {
return {}; return {};
@ -523,11 +552,9 @@ RoundVideoResult RoundVideoRecorder::Private::appendToPrevious(
fail(Error::Encoding); fail(Error::Encoding);
return {}; return {};
} }
return RoundVideoResult{ video.content = base::take(_result);
.content = base::take(_result), video.duration += _previous.duration;
.waveform = QByteArray(), return video;
.duration = _previous.duration + video.duration,
};
} }
FormatPointer RoundVideoRecorder::Private::OpenInputContext( FormatPointer RoundVideoRecorder::Private::OpenInputContext(
@ -605,7 +632,11 @@ void RoundVideoRecorder::Private::restart(RoundVideoPartial partial) {
return; return;
} }
_previous = std::move(partial.video); _previous = std::move(partial.video);
_minithumbs = std::move(_previous.minithumbs);
_minithumbsCount = _previous.minithumbsCount;
Assert(_minithumbSize == _previous.minithumbSize);
_maxDuration = kMaxDuration - _previous.duration; _maxDuration = kMaxDuration - _previous.duration;
_minithumbNextTimestamp = 0;
_finished = false; _finished = false;
initEncoding(); initEncoding();
_timeoutTimer.callOnce(kInitTimeout); _timeoutTimer.callOnce(kInitTimeout);
@ -720,6 +751,7 @@ void RoundVideoRecorder::Private::encodeVideoFrame(
cutCircleFromYUV420P(_videoFrame.get()); cutCircleFromYUV420P(_videoFrame.get());
_videoFrame->pts = mcstimestamp - _videoFirstTimestamp; _videoFrame->pts = mcstimestamp - _videoFirstTimestamp;
maybeSaveMinithumb(_videoFrame.get(), frame, crop);
if (_videoFrame->pts >= _maxDuration * int64(1000)) { if (_videoFrame->pts >= _maxDuration * int64(1000)) {
notifyFinished(); notifyFinished();
return; return;
@ -728,6 +760,49 @@ void RoundVideoRecorder::Private::encodeVideoFrame(
} }
} }
void RoundVideoRecorder::Private::maybeSaveMinithumb(
not_null<AVFrame*> frame,
const QImage &original,
QRect crop) {
if (frame->pts < _minithumbNextTimestamp * 1000) {
return;
}
_minithumbNextTimestamp += crl::time(1000) / kMinithumbsPerSecond;
const auto perline = original.bytesPerLine();
const auto perpixel = original.depth() / 8;
const auto cropped = QImage(
original.constBits() + (crop.y() * perline) + (crop.x() * perpixel),
crop.width(),
crop.height(),
perline,
original.format()
).scaled(
_minithumbSize,
_minithumbSize,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
const auto row = _minithumbsCount / kMinithumbsInRow;
const auto column = _minithumbsCount % kMinithumbsInRow;
const auto fromPerLine = cropped.bytesPerLine();
auto from = cropped.constBits();
const auto toPerLine = _minithumbs.bytesPerLine();
const auto toPerPixel = _minithumbs.depth() / 8;
auto to = _minithumbs.bits()
+ (row * _minithumbSize * toPerLine)
+ (column * _minithumbSize * toPerPixel);
Assert(toPerPixel == perpixel);
for (auto y = 0; y != _minithumbSize; ++y) {
Assert(to + toPerLine - _minithumbs.constBits()
<= _minithumbs.bytesPerLine() * _minithumbs.height());
memcpy(to, from, _minithumbSize * toPerPixel);
from += fromPerLine;
to += toPerLine;
}
++_minithumbsCount;
}
void RoundVideoRecorder::Private::initCircleMask() { void RoundVideoRecorder::Private::initCircleMask() {
const auto width = kSide; const auto width = kSide;
const auto height = kSide; const auto height = kSide;
@ -747,6 +822,16 @@ void RoundVideoRecorder::Private::initCircleMask() {
} }
} }
void RoundVideoRecorder::Private::initMinithumbsCanvas() {
const auto width = kMinithumbsInRow * _minithumbSize;
const auto seconds = (kMaxDuration + 999) / 1000;
const auto persecond = kMinithumbsPerSecond;
const auto frames = (seconds + persecond - 1) * persecond;
const auto rows = (frames + kMinithumbsInRow - 1) / kMinithumbsInRow;
const auto height = rows * _minithumbSize;
_minithumbs = QImage(width, height, QImage::Format_ARGB32_Premultiplied);
}
void RoundVideoRecorder::Private::cutCircleFromYUV420P( void RoundVideoRecorder::Private::cutCircleFromYUV420P(
not_null<AVFrame*> frame) { not_null<AVFrame*> frame) {
const auto width = frame->width; const auto width = frame->width;
@ -888,9 +973,9 @@ bool RoundVideoRecorder::Private::writeFrame(
while (true) { while (true) {
error = AvErrorWrap(avcodec_receive_packet(codec.get(), pkt)); error = AvErrorWrap(avcodec_receive_packet(codec.get(), pkt));
if (error.code() == AVERROR(EAGAIN)) { if (error.code() == AVERROR(EAGAIN)) {
return true; // Need more input return true; // Need more input
} else if (error.code() == AVERROR_EOF) { } else if (error.code() == AVERROR_EOF) {
return true; // Encoding finished return true; // Encoding finished
} else if (error) { } else if (error) {
LogError("avcodec_receive_packet", error); LogError("avcodec_receive_packet", error);
fail(Error::Encoding); fail(Error::Encoding);
@ -945,7 +1030,7 @@ RoundVideoRecorder::RoundVideoRecorder(
RoundVideoRecorderDescriptor &&descriptor) RoundVideoRecorderDescriptor &&descriptor)
: _descriptor(std::move(descriptor)) : _descriptor(std::move(descriptor))
, _preview(std::make_unique<RpWidget>(_descriptor.container)) , _preview(std::make_unique<RpWidget>(_descriptor.container))
, _private() { , _private(MinithumbSize()) {
setup(); setup();
} }

View file

@ -40,8 +40,11 @@ struct RoundVideoRecorderDescriptor {
struct RoundVideoResult { struct RoundVideoResult {
QByteArray content; QByteArray content;
QByteArray waveform; QVector<signed char> waveform;
crl::time duration = 0; crl::time duration = 0;
QImage minithumbs;
int minithumbsCount = 0;
int minithumbSize = 0;
}; };
struct RoundVideoPartial { struct RoundVideoPartial {