From 6247eea21f27fdc72c8176ad5b2513000f403a1a Mon Sep 17 00:00:00 2001 From: Andrew Wason Date: Tue, 23 Apr 2024 14:17:33 -0400 Subject: [PATCH] make Encoder independent of RenderSession --- src/MediaFX/app-encoder.qml | 9 ++-- src/MediaFX/encoder.cpp | 84 +++++++++++++++++++++++++--------- src/MediaFX/encoder.h | 36 +++++++++++---- src/MediaFX/main.cpp | 2 +- src/MediaFX/render_context.cpp | 2 +- src/MediaFX/render_context.h | 24 ++++++++-- tests/tst_encoder.cpp | 30 ++++++------ 7 files changed, 136 insertions(+), 51 deletions(-) diff --git a/src/MediaFX/app-encoder.qml b/src/MediaFX/app-encoder.qml index 50e6781..05da91b 100644 --- a/src/MediaFX/app-encoder.qml +++ b/src/MediaFX/app-encoder.qml @@ -5,8 +5,8 @@ import QtQuick RenderWindow { id: renderWindow - width: renderSession.renderContext.frameSize.width - height: renderSession.renderContext.frameSize.height + width: RenderContext.frameSize.width + height: RenderContext.frameSize.height renderSession: renderSession Component.onCompleted: { @@ -24,6 +24,9 @@ RenderWindow { } Encoder { id: encoder - renderContext: renderSession.renderContext + frameSize: RenderContext.frameSize + frameRate: RenderContext.frameRate + sampleRate: RenderContext.sampleRate + outputFileName: RenderContext.outputFileName } } diff --git a/src/MediaFX/encoder.cpp b/src/MediaFX/encoder.cpp index fcae9c4..617b3f8 100644 --- a/src/MediaFX/encoder.cpp +++ b/src/MediaFX/encoder.cpp @@ -41,6 +41,8 @@ extern "C" { // NOLINTBEGIN(bugprone-assignment-in-if-condition) +const Rational Encoder::DefaultFrameRate = { 30, 1 }; + Encoder::Encoder(QObject* parent) : QObject(parent) { @@ -54,25 +56,66 @@ Encoder::~Encoder() avformat_free_context(m_formatContext); } -void Encoder::setRenderContext(const RenderContext* renderContext) +void Encoder::setOutputFileName(const QString& outputFileName) { - if (m_renderContext) { - qmlWarning(this) << "Encoder renderContext is a write-once property and cannot be changed"; - return; + if (m_outputFileName != outputFileName) { + if (!m_outputFileName.isEmpty()) { + qmlWarning(this) << "Encoder outputFileName is a write-once property and cannot be changed"; + return; + } + m_outputFileName = outputFileName; + emit outputFileNameChanged(); } - if (renderContext != m_renderContext) { - m_renderContext = renderContext; - initialize(m_renderContext); - emit renderContextChanged(); +} + +void Encoder::setFrameSize(const QSize& frameSize) +{ + if (m_frameSize != frameSize) { + if (!m_frameSize.isEmpty()) { + qmlWarning(this) << "Encoder frameSize is a write-once property and cannot be changed"; + return; + } + m_frameSize = frameSize; + emit frameSizeChanged(); } } -void Encoder::initialize(const RenderContext* renderContext) +void Encoder::setFrameRate(const Rational& frameRate) { + if (m_frameRate != frameRate) { + if (m_frameRate != DefaultFrameRate) { + qmlWarning(this) << "Encoder frameRate is a write-once property and cannot be changed"; + return; + } + m_frameRate = frameRate; + emit frameRateChanged(); + } +} + +void Encoder::setSampleRate(int sampleRate) +{ + if (m_sampleRate != sampleRate) { + if (m_sampleRate != DefaultSampleRate) { + qmlWarning(this) << "Encoder sampleRate is a write-once property and cannot be changed"; + return; + } + m_sampleRate = sampleRate; + emit sampleRateChanged(); + } +} + +void Encoder::initialize() +{ + if (m_outputFileName.isEmpty() || m_frameSize.isEmpty()) { + qmlWarning(this) << "Encoder not initialized"; + emit encodingError(); + return; + } + int ret = 0; // Select nut format - if ((ret = avformat_alloc_output_context2(&m_formatContext, nullptr, "nut", qUtf8Printable(renderContext->outputFileName()))) < 0) { + if ((ret = avformat_alloc_output_context2(&m_formatContext, nullptr, "nut", qUtf8Printable(outputFileName()))) < 0) { qmlWarning(this) << "Could not allocate an output context, error:" << av_err2qstring(ret); return; } @@ -83,9 +126,9 @@ void Encoder::initialize(const RenderContext* renderContext) return; AVCodecContext* videoCodecContext = video->codecContext(); videoCodecContext->pix_fmt = VideoPixelFormat_FFMPEG; - videoCodecContext->width = renderContext->data().frameSize().width(); - videoCodecContext->height = renderContext->data().frameSize().height(); - AVRational timeBase(av_inv_q(renderContext->data().frameRate())); + videoCodecContext->width = frameSize().width(); + videoCodecContext->height = frameSize().height(); + AVRational timeBase(av_inv_q(frameRate())); int64_t gcd = av_gcd(FFABS(timeBase.num), FFABS(timeBase.den)); if (gcd) { timeBase.num = FFABS(timeBase.num) / gcd; @@ -101,7 +144,7 @@ void Encoder::initialize(const RenderContext* renderContext) return; AVCodecContext* audioCodecContext = audio->codecContext(); audioCodecContext->sample_fmt = AudioSampleFormat_FFMPEG; - audioCodecContext->sample_rate = renderContext->data().sampleRate(); + audioCodecContext->sample_rate = sampleRate(); #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 37, 100) audioCodecContext->channel_layout = AudioChannelLayout_FFMPEG; audioCodecContext->channels = av_get_channel_layout_nb_channels(audioCodecContext->channel_layout); @@ -119,8 +162,8 @@ void Encoder::initialize(const RenderContext* renderContext) m_videoStream.swap(video); if (!(m_formatContext->flags & AVFMT_NOFILE)) { - if ((ret = avio_open(&m_formatContext->pb, qUtf8Printable(renderContext->outputFileName()), AVIO_FLAG_WRITE)) < 0) { - qmlWarning(this) << "Could not open output file" << renderContext->outputFileName() << ", avio_open:" << av_err2qstring(ret); + if ((ret = avio_open(&m_formatContext->pb, qUtf8Printable(outputFileName()), AVIO_FLAG_WRITE)) < 0) { + qmlWarning(this) << "Could not open output file" << outputFileName() << ", avio_open:" << av_err2qstring(ret); return; } } @@ -135,19 +178,18 @@ void Encoder::initialize(const RenderContext* renderContext) qmlWarning(this) << "Could not open output file, avio_open:" << av_err2qstring(ret); return; } + + m_isValid = true; } void Encoder::componentComplete() { - if (!m_renderContext) { - qmlWarning(this) << "Encoder renderContext is a required property"; - emit qmlEngine(this)->exit(1); - } + initialize(); } bool Encoder::encode(const QAudioBuffer& audioBuffer, const QByteArray& videoData) { - if (!m_renderContext) { + if (!m_isValid) { emit encodingError(); return false; } diff --git a/src/MediaFX/encoder.h b/src/MediaFX/encoder.h index c7c40a9..dfb027d 100644 --- a/src/MediaFX/encoder.h +++ b/src/MediaFX/encoder.h @@ -3,6 +3,7 @@ #pragma once +#include "render_context.h" #include #include #include @@ -11,8 +12,6 @@ #include #include Q_MOC_INCLUDE("output_stream.h") -Q_MOC_INCLUDE("render_context.h") -class RenderContext; class OutputStream; class QAudioBuffer; struct AVFormatContext; @@ -21,7 +20,10 @@ using namespace std::chrono; class Encoder : public QObject, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) - Q_PROPERTY(const RenderContext* renderContext READ renderContext WRITE setRenderContext NOTIFY renderContextChanged FINAL REQUIRED) + Q_PROPERTY(QString outputFileName READ outputFileName WRITE setOutputFileName NOTIFY outputFileNameChanged FINAL REQUIRED) + Q_PROPERTY(QSize frameSize READ frameSize WRITE setFrameSize NOTIFY frameSizeChanged FINAL REQUIRED) + Q_PROPERTY(Rational frameRate READ frameRate WRITE setFrameRate NOTIFY frameRateChanged FINAL) + Q_PROPERTY(int sampleRate READ sampleRate WRITE setSampleRate NOTIFY sampleRateChanged FINAL) QML_ELEMENT public: @@ -32,11 +34,25 @@ class Encoder : public QObject, public QQmlParserStatus { Encoder& operator=(Encoder&&) = delete; ~Encoder() override; - const RenderContext* renderContext() const { return m_renderContext; } - void setRenderContext(const RenderContext*); + const QString& outputFileName() const { return m_outputFileName; } + void setOutputFileName(const QString& outputFileName); + + const QSize& frameSize() const { return m_frameSize; } + void setFrameSize(const QSize& frameSize); + + const Rational& frameRate() const { return m_frameRate; } + void setFrameRate(const Rational& frameRate); + + int sampleRate() const { return m_sampleRate; } + void setSampleRate(int sampleRate); + + void initialize(); signals: - void renderContextChanged(); + void outputFileNameChanged(); + void frameSizeChanged(); + void frameRateChanged(); + void sampleRateChanged(); void encodingError(); public slots: @@ -50,9 +66,13 @@ public slots: private: Q_DISABLE_COPY(Encoder); - void initialize(const RenderContext* renderContext); + static const Rational DefaultFrameRate; + static const int DefaultSampleRate = 44100; - const RenderContext* m_renderContext = nullptr; + bool m_isValid = false; + QSize m_frameSize; + Rational m_frameRate = DefaultFrameRate; + int m_sampleRate = DefaultSampleRate; QString m_outputFileName; AVFormatContext* m_formatContext = nullptr; std::unique_ptr m_videoStream; diff --git a/src/MediaFX/main.cpp b/src/MediaFX/main.cpp index c223323..8e3d98e 100644 --- a/src/MediaFX/main.cpp +++ b/src/MediaFX/main.cpp @@ -84,7 +84,7 @@ int encoder(QGuiApplication& app, QCommandLineParser& parser) } else av_log_set_level(AV_LOG_WARNING); - AVRational frameRate { 0 }; + Rational frameRate { 0 }; if (av_parse_video_rate(&frameRate, qUtf8Printable(parser.value(u"fps"_s))) < 0) parser.showHelp(1); int width = 0, height = 0; diff --git a/src/MediaFX/render_context.cpp b/src/MediaFX/render_context.cpp index 79cef79..1011463 100644 --- a/src/MediaFX/render_context.cpp +++ b/src/MediaFX/render_context.cpp @@ -5,7 +5,7 @@ extern "C" { #include } -RenderContextData::RenderContextData(const QUrl& sourceUrl, const QString& outputFileName, const QSize& frameSize, const AVRational& frameRate, int sampleRate) +RenderContextData::RenderContextData(const QUrl& sourceUrl, const QString& outputFileName, const QSize& frameSize, const Rational& frameRate, int sampleRate) : m_sourceUrl(sourceUrl) , m_outputFileName(outputFileName) , m_frameSize(frameSize) diff --git a/src/MediaFX/render_context.h b/src/MediaFX/render_context.h index fdcea68..7fb8889 100644 --- a/src/MediaFX/render_context.h +++ b/src/MediaFX/render_context.h @@ -12,10 +12,23 @@ extern "C" { #include } +struct Rational : public AVRational { + Q_GADGET +public: + friend constexpr bool operator==(const Rational& lhs, const Rational& rhs) noexcept + { + return lhs.num == rhs.num && lhs.den == rhs.den; + } + friend constexpr bool operator!=(const Rational& lhs, const Rational& rhs) noexcept + { + return lhs.num != rhs.num || lhs.den != rhs.den; + } +}; + class RenderContextData { public: // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - explicit RenderContextData(const QUrl& sourceUrl = QUrl(), const QString& outputFileName = "", const QSize& frameSize = QSize(640, 360), const AVRational& frameRate = { 30, 1 }, int sampleRate = 44100); + explicit RenderContextData(const QUrl& sourceUrl = QUrl(), const QString& outputFileName = "", const QSize& frameSize = QSize(640, 360), const Rational& frameRate = { 30, 1 }, int sampleRate = 44100); RenderContextData(RenderContextData&&) = default; RenderContextData(const RenderContextData&) = default; @@ -26,12 +39,12 @@ class RenderContextData { constexpr const QUrl& sourceUrl() const { return m_sourceUrl; } constexpr const QString& outputFileName() const { return m_outputFileName; } constexpr const QSize& frameSize() const noexcept { return m_frameSize; } - constexpr const AVRational& frameRate() const noexcept { return m_frameRate; } + constexpr const Rational& frameRate() const noexcept { return m_frameRate; } constexpr int sampleRate() const noexcept { return m_sampleRate; } private: QSize m_frameSize; - AVRational m_frameRate; + Rational m_frameRate; int m_sampleRate; QUrl m_sourceUrl; QString m_outputFileName; @@ -41,7 +54,9 @@ class RenderContext : public QObject { Q_OBJECT Q_PROPERTY(QUrl sourceUrl READ sourceUrl CONSTANT) Q_PROPERTY(QString outputFileName READ outputFileName CONSTANT) + Q_PROPERTY(int sampleRate READ sampleRate CONSTANT) Q_PROPERTY(QSize frameSize READ frameSize CONSTANT) + Q_PROPERTY(Rational frameRate READ frameRate CONSTANT) QML_ELEMENT QML_SINGLETON public: @@ -53,7 +68,10 @@ class RenderContext : public QObject { constexpr const QUrl& sourceUrl() const { return m_data.sourceUrl(); } constexpr const QString& outputFileName() const { return m_data.outputFileName(); } + constexpr int sampleRate() const noexcept { return m_data.sampleRate(); } constexpr const QSize& frameSize() const noexcept { return m_data.frameSize(); } + constexpr const Rational& frameRate() const noexcept { return m_data.frameRate(); } + constexpr const RenderContextData& data() const noexcept { return m_data; } void setData(const RenderContextData& data) { m_data = data; } diff --git a/tests/tst_encoder.cpp b/tests/tst_encoder.cpp index be4e323..acd3373 100644 --- a/tests/tst_encoder.cpp +++ b/tests/tst_encoder.cpp @@ -49,26 +49,28 @@ private slots: QFile encodedFile(outputDir.filePath("encoder.nut")); - const RenderContextData renderContextData(QUrl(), encodedFile.fileName(), QSize(160, 120), AVRational { 5, 1 }, 44100); - RenderContext renderContext; - renderContext.setData(renderContextData); Encoder encoder; QSignalSpy spy(&encoder, SIGNAL(encodingError())); QVERIFY(spy.isValid()); - encoder.setRenderContext(&renderContext); + encoder.setOutputFileName(encodedFile.fileName()); + encoder.setFrameSize(QSize(160, 120)); + encoder.setFrameRate(Rational { 5, 1 }); + encoder.setSampleRate(44100); + encoder.initialize(); + QVERIFY(spy.empty()); QAudioFormat audioFormat; audioFormat.setSampleFormat(QAudioFormat::Float); audioFormat.setChannelConfig(QAudioFormat::ChannelConfigStereo); - audioFormat.setSampleRate(renderContext.data().sampleRate()); - QAudioBuffer audioBuffer(audioFormat.framesForDuration(frameRateToFrameDuration(renderContext.data().frameRate()).count()), audioFormat); + audioFormat.setSampleRate(encoder.sampleRate()); + QAudioBuffer audioBuffer(audioFormat.framesForDuration(frameRateToFrameDuration(encoder.frameRate()).count()), audioFormat); - QByteArray videoData(static_cast(renderContext.data().frameSize().width() * renderContext.data().frameSize().height() * 4), Qt::Uninitialized); + QByteArray videoData(static_cast(encoder.frameSize().width() * encoder.frameSize().height() * 4), Qt::Uninitialized); - const int frames = static_cast(2.0 * av_q2d(renderContext.data().frameRate())); // 2 seconds + const int frames = static_cast(2.0 * av_q2d(encoder.frameRate())); // 2 seconds double audioTime = 0; - double audioIncr = 2 * M_PI * 110.0 / renderContext.data().sampleRate(); - double audioIncr2 = 2 * M_PI * 110.0 / renderContext.data().sampleRate() / renderContext.data().sampleRate(); + double audioIncr = 2 * M_PI * 110.0 / encoder.sampleRate(); + double audioIncr2 = 2 * M_PI * 110.0 / encoder.sampleRate() / encoder.sampleRate(); for (int i = 0; i < frames; i++) { // Audio sine wave @@ -81,12 +83,12 @@ private slots: audioIncr += audioIncr2; } // Video RGBA colors - int step = static_cast(av_q2d(renderContext.data().frameRate()) * i); - for (int y = 0; y < renderContext.data().frameSize().height(); y++) { - for (int x = 0; x < renderContext.data().frameSize().width(); x++) { + int step = static_cast(av_q2d(encoder.frameRate()) * i); + for (int y = 0; y < encoder.frameSize().height(); y++) { + for (int x = 0; x < encoder.frameSize().width(); x++) { // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - uint8_t* pixel = reinterpret_cast(&(videoData.data()[static_cast((y * renderContext.frameSize().width() + x) * 4)])); + uint8_t* pixel = reinterpret_cast(&(videoData.data()[static_cast((y * encoder.frameSize().width() + x) * 4)])); pixel[0] = x + y + step * 3; pixel[1] = 128 + y + step * 2; pixel[2] = 64 + x + step * 5;