From 4e8a1f8d29c2b37cf1c5c041abe13904637e84f1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 1 Oct 2020 10:47:03 +0300 Subject: [PATCH] Fix voice messages sending. --- .../SourceFiles/history/history_widget.cpp | 46 +++-- Telegram/SourceFiles/history/history_widget.h | 7 +- .../view/history_view_compose_controls.cpp | 51 ++--- .../view/history_view_compose_controls.h | 1 + .../media/audio/media_audio_capture.cpp | 181 +++++++++++------- .../media/audio/media_audio_capture.h | 69 ++----- 6 files changed, 186 insertions(+), 169 deletions(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 20c36f6ef..79b2d295f 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -232,10 +232,6 @@ HistoryWidget::HistoryWidget( initTabbedSelector(); - connect(Media::Capture::instance(), SIGNAL(error()), this, SLOT(onRecordError())); - connect(Media::Capture::instance(), SIGNAL(updated(quint16,qint32)), this, SLOT(onRecordUpdate(quint16,qint32))); - connect(Media::Capture::instance(), SIGNAL(done(QByteArray,VoiceWaveform,qint32)), this, SLOT(onRecordDone(QByteArray,VoiceWaveform,qint32))); - _attachToggle->addClickHandler(App::LambdaDelayed( st::historyAttach.ripple.hideDuration, this, @@ -1367,15 +1363,13 @@ void HistoryWidget::setInnerFocus() { } } -void HistoryWidget::onRecordError() { - stopRecording(false); -} - -void HistoryWidget::onRecordDone( +void HistoryWidget::recordDone( QByteArray result, VoiceWaveform waveform, - qint32 samples) { - if (!canWriteMessage() || result.isEmpty()) return; + int samples) { + if (!canWriteMessage() || result.isEmpty()) { + return; + } Window::ActivateWindow(controller()); const auto duration = samples / Media::Player::kDefaultFrequency; @@ -1384,7 +1378,7 @@ void HistoryWidget::onRecordDone( session().api().sendVoiceMessage(result, waveform, duration, action); } -void HistoryWidget::onRecordUpdate(quint16 level, qint32 samples) { +void HistoryWidget::recordUpdate(ushort level, int samples) { if (!_recording) { return; } @@ -3361,6 +3355,8 @@ void HistoryWidget::leaveToChildEvent(QEvent *e, QWidget *child) { // e -- from } void HistoryWidget::recordStartCallback() { + using namespace Media::Capture; + const auto error = _peer ? Data::RestrictionError(_peer, ChatRestriction::f_send_media) : std::nullopt; @@ -3369,11 +3365,17 @@ void HistoryWidget::recordStartCallback() { return; } else if (showSlowmodeError()) { return; - } else if (!Media::Capture::instance()->available()) { + } else if (!instance()->available()) { return; } - emit Media::Capture::instance()->start(); + instance()->start(); + instance()->updated( + ) | rpl::start_with_next_error([=](const Update &update) { + recordUpdate(update.level, update.samples); + }, [=] { + stopRecording(false); + }, _recordingLifetime); _recording = _inField = true; updateControlsVisibility(); @@ -3403,11 +3405,20 @@ void HistoryWidget::mouseReleaseEvent(QMouseEvent *e) { } void HistoryWidget::stopRecording(bool send) { - emit Media::Capture::instance()->stop(send); + if (send) { + const auto weak = Ui::MakeWeak(this); + Media::Capture::instance()->stop(crl::guard(this, [=]( + const Media::Capture::Result &result) { + recordDone(result.bytes, result.waveform, result.samples); + })); + } else { + Media::Capture::instance()->stop(); + } _recordingLevel = anim::value(); _recordingAnimation.stop(); + _recordingLifetime.destroy(); _recording = false; _recordingSamples = 0; if (_history) { @@ -3662,7 +3673,10 @@ bool HistoryWidget::isMuteUnmute() const { } bool HistoryWidget::showRecordButton() const { - return Media::Capture::instance()->available() && !HasSendText(_field) && !readyToForward() && !_editMsgId; + return Media::Capture::instance()->available() + && !HasSendText(_field) + && !readyToForward() + && !_editMsgId; } bool HistoryWidget::showInlineBotCancel() const { diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 934c907a0..d1e541017 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -312,10 +312,6 @@ public slots: void onDraftSave(bool delayed = false); void onCloudDraftSave(); - void onRecordError(); - void onRecordDone(QByteArray result, VoiceWaveform waveform, qint32 samples); - void onRecordUpdate(quint16 level, qint32 samples); - void onUpdateHistoryItems(); // checks if we are too close to the top or to the bottom @@ -412,6 +408,8 @@ private: void animationCallback(); void updateOverStates(QPoint pos); + void recordDone(QByteArray result, VoiceWaveform waveform, int samples); + void recordUpdate(ushort level, int samples); void recordStartCallback(); void recordStopCallback(bool active); void recordUpdateCallback(QPoint globalPos); @@ -707,6 +705,7 @@ private: bool _inClickable = false; int _recordingSamples = 0; int _recordCancelWidth; + rpl::lifetime _recordingLifetime; // This can animate for a very long time (like in music playing), // so it should be a Basic, not a Simple animation. diff --git a/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp index aa430164e..c033bfe16 100644 --- a/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp @@ -786,25 +786,6 @@ void ComposeControls::init() { initSendButton(); initWriteRestriction(); - QObject::connect( - ::Media::Capture::instance(), - &::Media::Capture::Instance::error, - _wrap.get(), - [=] { recordError(); }); - QObject::connect( - ::Media::Capture::instance(), - &::Media::Capture::Instance::updated, - _wrap.get(), - [=](quint16 level, int samples) { recordUpdated(level, samples); }); - qRegisterMetaType(); - QObject::connect( - ::Media::Capture::instance(), - &::Media::Capture::Instance::done, - _wrap.get(), - [=](QByteArray result, VoiceWaveform waveform, int samples) { - recordDone(result, waveform, samples); - }); - _wrap->sizeValue( ) | rpl::start_with_next([=](QSize size) { updateControlsGeometry(size); @@ -855,10 +836,6 @@ void ComposeControls::init() { } } -void ComposeControls::recordError() { - stopRecording(false); -} - void ComposeControls::recordDone( QByteArray result, VoiceWaveform waveform, @@ -889,20 +866,26 @@ void ComposeControls::recordUpdated(quint16 level, int samples) { } void ComposeControls::recordStartCallback() { - //const auto error = _peer // #TODO restrictions - // ? Data::RestrictionError(_peer, ChatRestriction::f_send_media) - // : std::nullopt; - const auto error = std::optional(); + using namespace ::Media::Capture; + const auto error = _history + ? Data::RestrictionError(_history->peer, ChatRestriction::f_send_media) + : std::nullopt; if (error) { Ui::show(Box(*error)); return; } else if (_showSlowmodeError && _showSlowmodeError()) { return; - } else if (!::Media::Capture::instance()->available()) { + } else if (!instance()->available()) { return; } - emit ::Media::Capture::instance()->start(); + instance()->start(); + instance()->updated( + ) | rpl::start_with_next_error([=](const Update &update) { + recordUpdated(update.level, update.samples); + }, [=] { + stopRecording(false); + }, _recordingLifetime); _recording = _inField = true; updateControlsVisibility(); @@ -922,11 +905,19 @@ void ComposeControls::recordUpdateCallback(QPoint globalPos) { } void ComposeControls::stopRecording(bool send) { - emit ::Media::Capture::instance()->stop(send); + if (send) { + ::Media::Capture::instance()->stop(crl::guard(_wrap.get(), [=]( + const ::Media::Capture::Result &result) { + recordDone(result.bytes, result.waveform, result.samples); + })); + } else { + ::Media::Capture::instance()->stop(); + } _recordingLevel = anim::value(); _recordingAnimation.stop(); + _recordingLifetime.destroy(); _recording = false; _recordingSamples = 0; _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 }); diff --git a/Telegram/SourceFiles/history/view/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/history_view_compose_controls.h index 4c1167e3a..68f008206 100644 --- a/Telegram/SourceFiles/history/view/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/history_view_compose_controls.h @@ -230,6 +230,7 @@ private: //bool _inClickable = false; int _recordingSamples = 0; int _recordCancelWidth; + rpl::lifetime _recordingLifetime; rpl::lifetime _uploaderSubscriptions; diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp index d2ce3a392..7bbdf97de 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp +++ b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio_capture.h" #include "media/audio/media_audio_ffmpeg_loader.h" +#include "base/timer.h" #include #include @@ -37,6 +38,36 @@ bool ErrorHappened(ALCdevice *device) { } // namespace +class Instance::Inner final : public QObject { +public: + Inner(QThread *thread); + ~Inner(); + + void start(Fn updated, Fn error); + void stop(Fn callback = nullptr); + + void timeout(); + +private: + void processFrame(int32 offset, int32 framesize); + void fail(); + + void writeFrame(AVFrame *frame); + + // Writes the packets till EAGAIN is got from av_receive_packet() + // Returns number of packets written or -1 on error + int writePackets(); + + Fn _updated; + Fn _error; + + struct Private; + std::unique_ptr d; + base::Timer _timer; + QByteArray _captured; + +}; + void Start() { Assert(CaptureInstance == nullptr); CaptureInstance = new Instance(); @@ -47,18 +78,32 @@ void Finish() { delete base::take(CaptureInstance); } -Instance::Instance() : _inner(new Inner(&_thread)) { +Instance::Instance() : _inner(std::make_unique(&_thread)) { CaptureInstance = this; - connect(this, SIGNAL(start()), _inner, SLOT(onStart())); - connect(this, SIGNAL(stop(bool)), _inner, SLOT(onStop(bool))); - connect(_inner, SIGNAL(done(QByteArray, VoiceWaveform, qint32)), this, SIGNAL(done(QByteArray, VoiceWaveform, qint32))); - connect(_inner, SIGNAL(updated(quint16, qint32)), this, SIGNAL(updated(quint16, qint32))); - connect(_inner, SIGNAL(error()), this, SIGNAL(error())); - connect(&_thread, SIGNAL(started()), _inner, SLOT(onInit())); - connect(&_thread, SIGNAL(finished()), _inner, SLOT(deleteLater())); _thread.start(); } +void Instance::start() { + _updates.fire_done(); + InvokeQueued(_inner.get(), [=] { + _inner->start([=](Update update) { + crl::on_main(this, [=] { + _updates.fire_copy(update); + }); + }, [=] { + crl::on_main(this, [=] { + _updates.fire_error({}); + }); + }); + }); +} + +void Instance::stop(Fn callback) { + InvokeQueued(_inner.get(), [=] { + _inner->stop(callback); + }); +} + void Instance::check() { _available = false; if (auto device = alcGetString(0, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER)) { @@ -71,7 +116,8 @@ void Instance::check() { } Instance::~Instance() { - _inner = nullptr; + InvokeQueued(_inner.get(), [copy = base::take(_inner)] { + }); _thread.quit(); _thread.wait(); } @@ -155,34 +201,39 @@ struct Instance::Inner::Private { } }; -Instance::Inner::Inner(QThread *thread) : d(new Private()) { +Instance::Inner::Inner(QThread *thread) +: d(std::make_unique()) +, _timer(thread, [=] { timeout(); }) { moveToThread(thread); - _timer.moveToThread(thread); - connect(&_timer, SIGNAL(timeout()), this, SLOT(onTimeout())); } Instance::Inner::~Inner() { - onStop(false); - delete d; + stop(); } -void Instance::Inner::onInit() { +void Instance::Inner::fail() { + Expects(_error != nullptr); + + stop(); + _error(); } -void Instance::Inner::onStart() { +void Instance::Inner::start(Fn updated, Fn error) { + _updated = std::move(updated); + _error = std::move(_error); // Start OpenAL Capture d->device = alcCaptureOpenDevice(nullptr, kCaptureFrequency, AL_FORMAT_MONO16, kCaptureFrequency / 5); if (!d->device) { LOG(("Audio Error: capture device not present!")); - emit error(); + fail(); return; } alcCaptureStart(d->device); if (ErrorHappened(d->device)) { alcCaptureCloseDevice(d->device); d->device = nullptr; - emit error(); + fail(); return; } @@ -190,7 +241,7 @@ void Instance::Inner::onStart() { d->ioBuffer = (uchar*)av_malloc(AVBlockSize); - d->ioContext = avio_alloc_context(d->ioBuffer, AVBlockSize, 1, static_cast(d), &Private::_read_data, &Private::_write_data, &Private::_seek_data); + d->ioContext = avio_alloc_context(d->ioBuffer, AVBlockSize, 1, static_cast(d.get()), &Private::_read_data, &Private::_write_data, &Private::_seek_data); int res = 0; char err[AV_ERROR_MAX_STRING_SIZE] = { 0 }; AVOutputFormat *fmt = 0; @@ -201,15 +252,13 @@ void Instance::Inner::onStart() { } if (!fmt) { LOG(("Audio Error: Unable to find opus AVOutputFormat for capture")); - onStop(false); - emit error(); + fail(); return; } if ((res = avformat_alloc_output_context2(&d->fmtContext, 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))); - onStop(false); - emit error(); + fail(); return; } d->fmtContext->pb = d->ioContext; @@ -220,23 +269,20 @@ void Instance::Inner::onStart() { d->codec = avcodec_find_encoder(fmt->audio_codec); if (!d->codec) { LOG(("Audio Error: Unable to avcodec_find_encoder for capture")); - onStop(false); - emit error(); + fail(); return; } d->stream = avformat_new_stream(d->fmtContext, d->codec); if (!d->stream) { LOG(("Audio Error: Unable to avformat_new_stream for capture")); - onStop(false); - emit error(); + fail(); return; } 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")); - onStop(false); - emit error(); + fail(); return; } @@ -255,8 +301,7 @@ void Instance::Inner::onStart() { // 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))); - onStop(false); - emit error(); + fail(); return; } @@ -287,48 +332,46 @@ void Instance::Inner::onStart() { 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))); - onStop(false); - emit error(); + fail(); return; } d->maxDstSamples = d->srcSamples; if ((res = av_samples_alloc_array_and_samples(&d->dstSamplesData, 0, d->codecContext->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))); - onStop(false); - emit error(); + fail(); return; } d->dstSamplesSize = av_samples_get_buffer_size(0, d->codecContext->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))); - onStop(false); - emit error(); + fail(); return; } // 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))); - onStop(false); - emit error(); + fail(); return; } - _timer.start(50); + _timer.callEach(50); _captured.clear(); _captured.reserve(kCaptureBufferSlice); DEBUG_LOG(("Audio Capture: started!")); } -void Instance::Inner::onStop(bool needResult) { - if (!_timer.isActive()) return; // in onStop() already - _timer.stop(); +void Instance::Inner::stop(Fn callback) { + if (!_timer.isActive()) { + return; // in stop() already + } + _timer.cancel(); if (d->device) { alcCaptureStop(d->device); - onTimeout(); // get last data + timeout(); // get last data } // Write what is left @@ -370,7 +413,11 @@ void Instance::Inner::onStop(bool needResult) { } } } - DEBUG_LOG(("Audio Capture: stopping (need result: %1), size: %2, samples: %3").arg(Logs::b(needResult)).arg(d->data.size()).arg(d->fullSamples)); + DEBUG_LOG(("Audio Capture: " + "stopping (need result: %1), size: %2, samples: %3" + ).arg(Logs::b(callback != nullptr) + ).arg(d->data.size() + ).arg(d->fullSamples)); _captured = QByteArray(); // Finish stream @@ -465,19 +512,21 @@ void Instance::Inner::onStop(bool needResult) { d->waveformPeak = 0; d->waveform.clear(); } - if (needResult) emit done(result, waveform, samples); + + if (callback) { + callback({ result, waveform, samples }); + } } -void Instance::Inner::onTimeout() { +void Instance::Inner::timeout() { if (!d->device) { - _timer.stop(); + _timer.cancel(); return; } ALint samples; alcGetIntegerv(d->device, ALC_CAPTURE_SAMPLES, sizeof(samples), &samples); if (ErrorHappened(d->device)) { - onStop(false); - emit error(); + fail(); return; } if (samples > 0) { @@ -490,8 +539,7 @@ void Instance::Inner::onTimeout() { _captured.resize(news); alcCaptureSamples(d->device, (ALCvoid *)(_captured.data() + s), samples); if (ErrorHappened(d->device)) { - onStop(false); - emit error(); + fail(); return; } @@ -512,7 +560,7 @@ void Instance::Inner::onTimeout() { } qint32 samplesFull = d->fullSamples + _captured.size() / sizeof(short), samplesSinceUpdate = samplesFull - d->lastUpdate; if (samplesSinceUpdate > kCaptureUpdateDelta * kCaptureFrequency / 1000) { - emit updated(d->levelMax, samplesFull); + _updated(Update{ .samples = samplesFull, .level = d->levelMax }); d->lastUpdate = samplesFull; d->levelMax = 0; } @@ -539,8 +587,7 @@ void Instance::Inner::processFrame(int32 offset, int32 framesize) { if (framesize % sizeof(short)) { // in the middle of a sample LOG(("Audio Error: Bad framesize in writeFrame() for capture, framesize %1, %2").arg(framesize)); - onStop(false); - emit error(); + fail(); return; } auto samplesCnt = static_cast(framesize / sizeof(short)); @@ -587,8 +634,7 @@ void Instance::Inner::processFrame(int32 offset, int32 framesize) { av_freep(&d->dstSamplesData[0]); if ((res = av_samples_alloc(d->dstSamplesData, 0, d->codecContext->channels, d->dstSamples, d->codecContext->sample_fmt, 1)) < 0) { LOG(("Audio Error: Unable to av_samples_alloc for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - onStop(false); - emit error(); + fail(); return; } d->dstSamplesSize = av_samples_get_buffer_size(0, d->codecContext->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0); @@ -596,8 +642,7 @@ void Instance::Inner::processFrame(int32 offset, int32 framesize) { if ((res = swr_convert(d->swrContext, d->dstSamplesData, d->dstSamples, (const uint8_t **)srcSamplesData, d->srcSamples)) < 0) { LOG(("Audio Error: Unable to swr_convert for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - onStop(false); - emit error(); + fail(); return; } @@ -627,30 +672,26 @@ void Instance::Inner::writeFrame(AVFrame *frame) { if (packetsWritten < 0) { if (frame && packetsWritten == AVERROR_EOF) { LOG(("Audio Error: EOF in packets received when EAGAIN was got in avcodec_send_frame()")); - onStop(false); - emit error(); + fail(); } return; } else if (!packetsWritten) { LOG(("Audio Error: No packets received when EAGAIN was got in avcodec_send_frame()")); - onStop(false); - emit error(); + fail(); return; } res = avcodec_send_frame(d->codecContext, frame); } if (res < 0) { LOG(("Audio Error: Unable to avcodec_send_frame for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - onStop(false); - emit error(); + fail(); return; } if (!frame) { // drain if ((res = writePackets()) != AVERROR_EOF) { LOG(("Audio Error: not EOF in packets received when draining the codec, result %1").arg(res)); - onStop(false); - emit error(); + fail(); } } } @@ -672,8 +713,7 @@ int Instance::Inner::writePackets() { return res; } LOG(("Audio Error: Unable to avcodec_receive_packet for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - onStop(false); - emit error(); + fail(); return res; } @@ -681,8 +721,7 @@ int Instance::Inner::writePackets() { pkt.stream_index = d->stream->index; if ((res = av_interleaved_write_frame(d->fmtContext, &pkt)) < 0) { LOG(("Audio Error: Unable to av_interleaved_write_frame for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - onStop(false); - emit error(); + fail(); return -1; } diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.h b/Telegram/SourceFiles/media/audio/media_audio_capture.h index b60c6422b..10ad66abc 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture.h +++ b/Telegram/SourceFiles/media/audio/media_audio_capture.h @@ -14,76 +14,49 @@ struct AVFrame; namespace Media { namespace Capture { +struct Update { + int samples = 0; + ushort level = 0; +}; + +struct Result { + QByteArray bytes; + VoiceWaveform waveform; + int samples = 0; +}; + void Start(); void Finish(); -class Instance : public QObject { - Q_OBJECT - +class Instance final : public QObject { public: Instance(); + ~Instance(); void check(); - bool available() const { + [[nodiscard]] bool available() const { return _available; } - ~Instance(); + [[nodiscard]] rpl::producer updated() const { + return _updates.events(); + } -signals: void start(); - void stop(bool needResult); - - void done(QByteArray data, VoiceWaveform waveform, qint32 samples); - void updated(quint16 level, qint32 samples); - void error(); + void stop(Fn callback = nullptr); private: class Inner; friend class Inner; bool _available = false; + rpl::event_stream _updates; QThread _thread; - Inner *_inner; + std::unique_ptr _inner; }; -Instance *instance(); - -class Instance::Inner : public QObject { - Q_OBJECT - -public: - Inner(QThread *thread); - ~Inner(); - -signals: - void error(); - void updated(quint16 level, qint32 samples); - void done(QByteArray data, VoiceWaveform waveform, qint32 samples); - -public slots: - void onInit(); - void onStart(); - void onStop(bool needResult); - - void onTimeout(); - -private: - void processFrame(int32 offset, int32 framesize); - - void writeFrame(AVFrame *frame); - - // Writes the packets till EAGAIN is got from av_receive_packet() - // Returns number of packets written or -1 on error - int writePackets(); - - struct Private; - Private *d; - QTimer _timer; - QByteArray _captured; - -}; +[[nodiscard]] Instance *instance(); } // namespace Capture } // namespace Media