Wait for both audio and video to start.

This commit is contained in:
John Preston 2024-10-18 17:36:04 +04:00
parent e59e4afd3e
commit 20a4c7f9f4
4 changed files with 115 additions and 39 deletions

View file

@ -144,7 +144,7 @@ void Instance::start(Fn<void(Chunk)> externalProcessing) {
}); });
}, [=] { }, [=] {
crl::on_main(this, [=] { crl::on_main(this, [=] {
_updates.fire_error({}); _updates.fire_error(Error::Other);
}); });
}, externalProcessing); }, externalProcessing);
crl::on_main(this, [=] { crl::on_main(this, [=] {

View file

@ -20,6 +20,15 @@ struct Update {
bool finished = false; bool finished = false;
}; };
enum class Error : uchar {
Other,
AudioInit,
VideoInit,
AudioTimeout,
VideoTimeout,
Encoding,
};
struct Chunk { struct Chunk {
crl::time finished = 0; crl::time finished = 0;
QByteArray samples; QByteArray samples;
@ -41,7 +50,7 @@ public:
return _available; return _available;
} }
[[nodiscard]] rpl::producer<Update, rpl::empty_error> updated() const { [[nodiscard]] rpl::producer<Update, Error> updated() const {
return _updates.events(); return _updates.events();
} }
@ -62,7 +71,7 @@ private:
bool _available = false; bool _available = false;
rpl::variable<bool> _started = false; rpl::variable<bool> _started = false;
rpl::event_stream<Update, rpl::empty_error> _updates; rpl::event_stream<Update, Error> _updates;
QThread _thread; QThread _thread;
std::unique_ptr<Inner> _inner; std::unique_ptr<Inner> _inner;

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "ui/controls/round_video_recorder.h" #include "ui/controls/round_video_recorder.h"
#include "base/concurrent_timer.h"
#include "base/debug_log.h" #include "base/debug_log.h"
#include "ffmpeg/ffmpeg_utility.h" #include "ffmpeg/ffmpeg_utility.h"
#include "media/audio/media_audio_capture.h" #include "media/audio/media_audio_capture.h"
@ -25,7 +26,8 @@ constexpr auto kUpdateEach = crl::time(100);
constexpr auto kAudioFrequency = 48'000; constexpr auto kAudioFrequency = 48'000;
constexpr auto kAudioBitRate = 32'000; constexpr auto kAudioBitRate = 32'000;
constexpr auto kVideoBitRate = 3 * 1024 * 1024; constexpr auto kVideoBitRate = 3 * 1024 * 1024;
constexpr auto kMaxDuration = 10 * crl::time(1000); AssertIsDebug(); constexpr auto kMaxDuration = 10 * crl::time(1000);
constexpr auto kInitTimeout = 5 * crl::time(1000);
using namespace FFmpeg; using namespace FFmpeg;
@ -40,7 +42,7 @@ public:
void push(const Media::Capture::Chunk &chunk); void push(const Media::Capture::Chunk &chunk);
using Update = Media::Capture::Update; using Update = Media::Capture::Update;
[[nodiscard]] rpl::producer<Update, rpl::empty_error> updated() const; [[nodiscard]] rpl::producer<Update, Error> updated() const;
[[nodiscard]] RoundVideoResult finish(); [[nodiscard]] RoundVideoResult finish();
@ -57,7 +59,8 @@ private:
void notifyFinished(); void notifyFinished();
void deinitEncoding(); void deinitEncoding();
void finishEncoding(); void finishEncoding();
void fail(); void fail(Error error);
void timeout();
void encodeVideoFrame(int64 mcstimestamp, const QImage &frame); void encodeVideoFrame(int64 mcstimestamp, const QImage &frame);
void encodeAudioFrame(const Media::Capture::Chunk &chunk); void encodeAudioFrame(const Media::Capture::Chunk &chunk);
@ -105,16 +108,21 @@ private:
ushort _maxLevelSinceLastUpdate = 0; ushort _maxLevelSinceLastUpdate = 0;
crl::time _lastUpdateDuration = 0; crl::time _lastUpdateDuration = 0;
rpl::event_stream<Update, rpl::empty_error> _updates; rpl::event_stream<Update, Error> _updates;
std::vector<bool> _circleMask; // Always nice to use vector<bool>! :D std::vector<bool> _circleMask; // Always nice to use vector<bool>! :D
base::ConcurrentTimer _timeoutTimer;
}; };
RoundVideoRecorder::Private::Private(crl::weak_on_queue<Private> weak) RoundVideoRecorder::Private::Private(crl::weak_on_queue<Private> weak)
: _weak(std::move(weak)) { : _weak(std::move(weak))
, _timeoutTimer(_weak, [=] { timeout(); }) {
initEncoding(); initEncoding();
initCircleMask(); initCircleMask();
_timeoutTimer.callOnce(kInitTimeout);
} }
RoundVideoRecorder::Private::~Private() { RoundVideoRecorder::Private::~Private() {
@ -174,8 +182,11 @@ void RoundVideoRecorder::Private::initEncoding() {
&Private::Seek, &Private::Seek,
"mp4"_q); "mp4"_q);
if (!initVideo() || !initAudio()) { if (!initVideo()) {
fail(); fail(Error::VideoInit);
return;
} else if (!initAudio()) {
fail(Error::AudioInit);
return; return;
} }
@ -184,7 +195,7 @@ void RoundVideoRecorder::Private::initEncoding() {
nullptr)); nullptr));
if (error) { if (error) {
LogError("avformat_write_header", error); LogError("avformat_write_header", error);
fail(); fail(Error::Encoding);
} }
} }
@ -364,7 +375,7 @@ void RoundVideoRecorder::Private::finishEncoding() {
} }
auto RoundVideoRecorder::Private::updated() const auto RoundVideoRecorder::Private::updated() const
-> rpl::producer<Update, rpl::empty_error> { -> rpl::producer<Update, Error> {
return _updates.events(); return _updates.events();
} }
@ -380,11 +391,19 @@ RoundVideoResult RoundVideoRecorder::Private::finish() {
}; };
}; };
void RoundVideoRecorder::Private::fail() { void RoundVideoRecorder::Private::fail(Error error) {
deinitEncoding(); deinitEncoding();
_updates.fire_error({}); _updates.fire_error({});
} }
void RoundVideoRecorder::Private::timeout() {
if (!_firstAudioChunkFinished) {
fail(Error::AudioTimeout);
} else if (!_firstVideoFrameTime) {
fail(Error::VideoTimeout);
}
}
void RoundVideoRecorder::Private::deinitEncoding() { void RoundVideoRecorder::Private::deinitEncoding() {
_swsContext = nullptr; _swsContext = nullptr;
_videoCodec = nullptr; _videoCodec = nullptr;
@ -448,7 +467,7 @@ void RoundVideoRecorder::Private::encodeVideoFrame(
AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P,
&_swsContext); &_swsContext);
if (!_swsContext) { if (!_swsContext) {
fail(); fail(Error::Encoding);
return; return;
} }
@ -579,7 +598,7 @@ void RoundVideoRecorder::Private::encodeAudioFrame(
if (error) { if (error) {
LogError("swr_convert", error); LogError("swr_convert", error);
fail(); fail(Error::Encoding);
return; return;
} }
@ -619,6 +638,8 @@ bool RoundVideoRecorder::Private::writeFrame(
const FramePointer &frame, const FramePointer &frame,
const CodecPointer &codec, const CodecPointer &codec,
AVStream *stream) { AVStream *stream) {
_timeoutTimer.cancel();
if (frame) { if (frame) {
updateResultDuration(frame->pts, codec->time_base); updateResultDuration(frame->pts, codec->time_base);
} }
@ -626,7 +647,7 @@ bool RoundVideoRecorder::Private::writeFrame(
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);
fail(); fail(Error::Encoding);
return false; return false;
} }
@ -642,7 +663,7 @@ 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);
fail(); fail(Error::Encoding);
return false; return false;
} }
@ -654,7 +675,7 @@ bool RoundVideoRecorder::Private::writeFrame(
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);
fail(); fail(Error::Encoding);
return false; return false;
} }
} }
@ -677,7 +698,11 @@ void RoundVideoRecorder::Private::updateResultDuration(
AVRational timeBase) { AVRational timeBase) {
accumulate_max(_resultDuration, PtsToTimeCeil(pts, timeBase)); accumulate_max(_resultDuration, PtsToTimeCeil(pts, timeBase));
if (_lastUpdateDuration + kUpdateEach >= _resultDuration) { const auto initial = !_lastUpdateDuration;
if (initial) {
accumulate_max(_resultDuration, crl::time(1));
}
if (initial || (_lastUpdateDuration + kUpdateEach < _resultDuration)) {
_lastUpdateDuration = _resultDuration; _lastUpdateDuration = _resultDuration;
_updates.fire({ _updates.fire({
.samples = int(_resultDuration * 48), .samples = int(_resultDuration * 48),
@ -704,15 +729,14 @@ Fn<void(Media::Capture::Chunk)> RoundVideoRecorder::audioChunkProcessor() {
}; };
} }
auto RoundVideoRecorder::updated() auto RoundVideoRecorder::updated() -> rpl::producer<Update, Error> {
-> rpl::producer<Update, rpl::empty_error> {
return _private.producer_on_main([](const Private &that) { return _private.producer_on_main([](const Private &that) {
return that.updated(); return that.updated();
}) | rpl::before_next([=](const Update &update) { }) | rpl::before_next(crl::guard(this, [=](const Update &update) {
const auto duration = (update.samples * crl::time(1000)) const auto progress = (update.samples * crl::time(1000))
/ kAudioFrequency; / float64(kAudioFrequency * kMaxDuration);
progressTo(duration / (1. * kMaxDuration)); progressTo(progress);
}); }));
} }
void RoundVideoRecorder::hide(Fn<void(RoundVideoResult)> done) { void RoundVideoRecorder::hide(Fn<void(RoundVideoResult)> done) {
@ -733,12 +757,20 @@ void RoundVideoRecorder::hide(Fn<void(RoundVideoResult)> done) {
void RoundVideoRecorder::progressTo(float64 progress) { void RoundVideoRecorder::progressTo(float64 progress) {
if (_progress == progress) { if (_progress == progress) {
return; return;
} else if (_progress > 0.001) {
_progressAnimation.start(
[=] { _preview->update(); },
_progress,
progress,
kUpdateEach * 1.1);
}
if (!_progress) {
_fadeContentAnimation.start(
[=] { _preview->update(); },
0.,
1.,
crl::time(200));
} }
_progressAnimation.start(
[=] { _preview->update(); },
progress,
_progress,
kUpdateEach);
_progress = progress; _progress = progress;
_preview->update(); _preview->update();
} }
@ -775,12 +807,12 @@ void RoundVideoRecorder::prepareFrame() {
void RoundVideoRecorder::createImages() { void RoundVideoRecorder::createImages() {
const auto ratio = style::DevicePixelRatio(); const auto ratio = style::DevicePixelRatio();
_framePrepared = QImage( _framePlaceholder = QImage(
QSize(_side, _side) * ratio, QSize(_side, _side) * ratio,
QImage::Format_ARGB32_Premultiplied); QImage::Format_ARGB32_Premultiplied);
_framePrepared.fill(Qt::transparent); _framePlaceholder.fill(Qt::transparent);
_framePrepared.setDevicePixelRatio(ratio); _framePlaceholder.setDevicePixelRatio(ratio);
auto p = QPainter(&_framePrepared); auto p = QPainter(&_framePlaceholder);
auto hq = PainterHighQualityEnabler(p); auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
@ -830,10 +862,23 @@ void RoundVideoRecorder::setup() {
prepareFrame(); prepareFrame();
auto p = QPainter(raw); auto p = QPainter(raw);
const auto opacity = _fadeAnimation.value(_visible ? 1. : 0.);
if (_fadeAnimation.animating()) {
p.setOpacity(opacity);
} else if (!_visible) {
return;
}
p.drawImage(raw->rect(), _shadow); p.drawImage(raw->rect(), _shadow);
const auto inner = QRect(_extent, _extent, _side, _side); const auto inner = QRect(_extent, _extent, _side, _side);
p.drawImage(inner, _framePrepared); if (!_progress) {
if (_progress > 0.) { p.drawImage(inner, _framePlaceholder);
} else {
if (_fadeContentAnimation.animating()) {
p.drawImage(inner, _framePlaceholder);
p.setOpacity(opacity * _fadeContentAnimation.value(1.));
}
p.drawImage(inner, _framePrepared);
auto hq = PainterHighQualityEnabler(p); auto hq = PainterHighQualityEnabler(p);
p.setPen(QPen( p.setPen(QPen(
Qt::white, Qt::white,
@ -867,10 +912,24 @@ void RoundVideoRecorder::setup() {
}, raw->lifetime()); }, raw->lifetime());
_descriptor.track->markFrameShown(); _descriptor.track->markFrameShown();
fade(true);
raw->show(); raw->show();
raw->raise(); raw->raise();
} }
void RoundVideoRecorder::fade(bool visible) {
if (_visible == visible) {
return;
}
_visible = visible;
_fadeAnimation.start(
[=] { _preview->update(); },
visible ? 0. : 1.,
visible ? 1. : 0.,
crl::time(200));
}
void RoundVideoRecorder::setPaused(bool paused) { void RoundVideoRecorder::setPaused(bool paused) {
if (_paused == paused) { if (_paused == paused) {
return; return;

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "base/weak_ptr.h"
#include "ui/effects/animations.h" #include "ui/effects/animations.h"
#include <crl/crl_object_on_queue.h> #include <crl/crl_object_on_queue.h>
@ -14,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Media::Capture { namespace Media::Capture {
struct Chunk; struct Chunk;
struct Update; struct Update;
enum class Error : uchar;
} // namespace Media::Capture } // namespace Media::Capture
namespace tgcalls { namespace tgcalls {
@ -42,7 +44,7 @@ struct RoundVideoResult {
crl::time duration = 0; crl::time duration = 0;
}; };
class RoundVideoRecorder final { class RoundVideoRecorder final : public base::has_weak_ptr {
public: public:
explicit RoundVideoRecorder(RoundVideoRecorderDescriptor &&descriptor); explicit RoundVideoRecorder(RoundVideoRecorderDescriptor &&descriptor);
~RoundVideoRecorder(); ~RoundVideoRecorder();
@ -53,7 +55,8 @@ public:
void hide(Fn<void(RoundVideoResult)> done = nullptr); void hide(Fn<void(RoundVideoResult)> done = nullptr);
using Update = Media::Capture::Update; using Update = Media::Capture::Update;
[[nodiscard]] rpl::producer<Update, rpl::empty_error> updated(); using Error = Media::Capture::Error;
[[nodiscard]] rpl::producer<Update, Error> updated();
private: private:
class Private; class Private;
@ -62,13 +65,17 @@ private:
void prepareFrame(); void prepareFrame();
void createImages(); void createImages();
void progressTo(float64 progress); void progressTo(float64 progress);
void fade(bool visible);
const RoundVideoRecorderDescriptor _descriptor; const RoundVideoRecorderDescriptor _descriptor;
std::unique_ptr<RpWidget> _preview; std::unique_ptr<RpWidget> _preview;
crl::object_on_queue<Private> _private; crl::object_on_queue<Private> _private;
Ui::Animations::Simple _progressAnimation; Ui::Animations::Simple _progressAnimation;
Ui::Animations::Simple _fadeAnimation;
Ui::Animations::Simple _fadeContentAnimation;
float64 _progress = 0.; float64 _progress = 0.;
QImage _frameOriginal; QImage _frameOriginal;
QImage _framePlaceholder;
QImage _framePrepared; QImage _framePrepared;
QImage _shadow; QImage _shadow;
int _lastAddedIndex = 0; int _lastAddedIndex = 0;
@ -76,6 +83,7 @@ private:
int _side = 0; int _side = 0;
int _progressStroke = 0; int _progressStroke = 0;
int _extent = 0; int _extent = 0;
bool _visible = false;
bool _paused = false; bool _paused = false;
}; };