Skip to content

Commit

Permalink
make Encoder independent of RenderSession
Browse files Browse the repository at this point in the history
  • Loading branch information
rectalogic committed Apr 23, 2024
1 parent 44596af commit 6247eea
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 51 deletions.
9 changes: 6 additions & 3 deletions src/MediaFX/app-encoder.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -24,6 +24,9 @@ RenderWindow {
}
Encoder {
id: encoder
renderContext: renderSession.renderContext
frameSize: RenderContext.frameSize
frameRate: RenderContext.frameRate
sampleRate: RenderContext.sampleRate
outputFileName: RenderContext.outputFileName
}
}
84 changes: 63 additions & 21 deletions src/MediaFX/encoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ extern "C" {

// NOLINTBEGIN(bugprone-assignment-in-if-condition)

const Rational Encoder::DefaultFrameRate = { 30, 1 };

Encoder::Encoder(QObject* parent)
: QObject(parent)
{
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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;
}
}
Expand All @@ -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;
}
Expand Down
36 changes: 28 additions & 8 deletions src/MediaFX/encoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#pragma once

#include "render_context.h"
#include <QObject>
#include <QQmlParserStatus>
#include <QString>
Expand All @@ -11,8 +12,6 @@
#include <chrono>
#include <memory>
Q_MOC_INCLUDE("output_stream.h")
Q_MOC_INCLUDE("render_context.h")
class RenderContext;
class OutputStream;
class QAudioBuffer;
struct AVFormatContext;
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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<OutputStream> m_videoStream;
Expand Down
2 changes: 1 addition & 1 deletion src/MediaFX/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/MediaFX/render_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ extern "C" {
#include <libavutil/rational.h>
}

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)
Expand Down
24 changes: 21 additions & 3 deletions src/MediaFX/render_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,23 @@ extern "C" {
#include <libavutil/rational.h>
}

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;
Expand All @@ -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;
Expand All @@ -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:
Expand All @@ -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; }

Expand Down
30 changes: 16 additions & 14 deletions tests/tst_encoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<microseconds>(renderContext.data().frameRate()).count()), audioFormat);
audioFormat.setSampleRate(encoder.sampleRate());
QAudioBuffer audioBuffer(audioFormat.framesForDuration(frameRateToFrameDuration<microseconds>(encoder.frameRate()).count()), audioFormat);

QByteArray videoData(static_cast<qsizetype>(renderContext.data().frameSize().width() * renderContext.data().frameSize().height() * 4), Qt::Uninitialized);
QByteArray videoData(static_cast<qsizetype>(encoder.frameSize().width() * encoder.frameSize().height() * 4), Qt::Uninitialized);

const int frames = static_cast<const int>(2.0 * av_q2d(renderContext.data().frameRate())); // 2 seconds
const int frames = static_cast<const int>(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
Expand All @@ -81,12 +83,12 @@ private slots:
audioIncr += audioIncr2;
}
// Video RGBA colors
int step = static_cast<int>(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<int>(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<uint8_t*>(&(videoData.data()[static_cast<ptrdiff_t>((y * renderContext.frameSize().width() + x) * 4)]));
uint8_t* pixel = reinterpret_cast<uint8_t*>(&(videoData.data()[static_cast<ptrdiff_t>((y * encoder.frameSize().width() + x) * 4)]));
pixel[0] = x + y + step * 3;
pixel[1] = 128 + y + step * 2;
pixel[2] = 64 + x + step * 5;
Expand Down

0 comments on commit 6247eea

Please sign in to comment.