Skip to content

Commit

Permalink
Make session available via attached property. Working.
Browse files Browse the repository at this point in the history
  • Loading branch information
rectalogic committed Apr 23, 2024
1 parent 948bf66 commit 8631802
Show file tree
Hide file tree
Showing 26 changed files with 273 additions and 157 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Item {
}
Component.onCompleted: {
videoClip.clipEnded.connect(RenderSession.endSession);
videoClip.clipEnded.connect(RenderSession.session.endSession);
}
}
VideoRenderer {
Expand All @@ -67,7 +67,7 @@ and renders one second of the video frames of it's [MediaClip.source](https://me
The [MultiEffect](https://doc.qt.io/qt-6/qml-qtquick-effects-multieffect.html)
filter is applied to the VideoRenderer to desaturate it.
When the clip finishes, it's [clipEnded](https://mediafx.org/qml-mediafx-mediaclip.html#clipEnded-signal) signal triggers the
[RenderSession.endSession](https://mediafx.org/qml-mediafx-rendersession.html#endSession-method) slot to end encoding.
[RenderSession.session.endSession](https://mediafx.org/qml-mediafx-rendersession.html#endSession-method) slot via the [RenderSession.session](XXX) attached property to end encoding.

See [Qt signals and slots](https://doc.qt.io/qt-6/qtqml-syntax-signals.html#connecting-signals-to-methods-and-signals).

Expand Down
47 changes: 27 additions & 20 deletions src/MediaFX/app-encoder.qml
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,39 @@

import QtQuick

RenderWindow {
id: renderWindow
width: RenderSession.renderContext.frameSize.width
height: RenderSession.renderContext.frameSize.height
RenderSession {
id: renderSession

Component.onCompleted: {
renderWindow.contentItem.enabled = false;
renderWindow.frameReady.connect(encoder.encode);
RenderSession.renderScene.connect(renderWindow.render);
RenderSession.sessionEnded.connect(encoder.finish);
encoder.encodingError.connect(RenderSession.fatalError);
RenderSession.beginSession();
}
RenderWindow {
id: renderWindow
width: renderSession.renderContext.frameSize.width
height: renderSession.renderContext.frameSize.height
renderSession: renderSession

Loader {
id: loader
source: RenderSession.renderContext.sourceUrl
anchors.fill: parent
Component.onCompleted: {
if (loader.status == Loader.Error)
RenderSession.fatalError();
renderWindow.contentItem.enabled = false;
renderWindow.frameReady.connect(encoder.encode);
renderSession.renderScene.connect(renderWindow.render);
renderSession.sessionEnded.connect(encoder.finish);
encoder.encodingError.connect(renderSession.fatalError);
renderSession.beginSession();
}
}

Loader {
id: loader
source: renderSession.renderContext.sourceUrl
anchors.fill: parent
Component.onCompleted: {
if (loader.status == Loader.Error)
renderSession.fatalError();
}
// This is made available on the nested QQmlContext the loaded component sees,
// used to implement RenderSession.session attached property
property RenderSession renderSession: renderSession
}
}
Encoder {
id: encoder
outputFileName: RenderSession.renderContext.outputFileName
renderContext: renderSession.renderContext
}
}
50 changes: 27 additions & 23 deletions src/MediaFX/audio_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "render_session.h"
#include <QAudioFormat>
#include <QObject>
#include <QQmlContext>
#include <QQmlEngine>
#include <QQmlInfo>
#include <QmlTypeAndRevisionsRegistration>
Expand All @@ -24,17 +25,26 @@ AudioRenderer::AudioRenderer(QObject* parent)

AudioRenderer::~AudioRenderer() = default;

void AudioRenderer::classBegin()
void AudioRenderer::componentComplete()
{
// We default to the root renderer as upstreamRenderer, unless we are root.
// The root renderer is created in C++ so classBegin() is not called.
upstreamRendererInternal()->addDownstreamRenderer(this);
if (!m_upstreamRenderer) {
// We default to the root renderer as upstreamRenderer, unless we are root.
// The root renderer is created in C++ so classBegin() is not called.
setUpstreamRenderer(rootAudioRenderer());
}
}

AudioRenderer* AudioRenderer::rootAudioRenderer()
{
if (!m_rootAudioRenderer)
m_rootAudioRenderer = qmlEngine(this)->singletonInstance<RenderSession*>("MediaFX", "RenderSession")->rootAudioRenderer();
if (!m_rootAudioRenderer) {
RenderSessionAttached* attached = qobject_cast<RenderSessionAttached*>(qmlAttachedPropertiesObject<RenderSession>(this));
if (attached && attached->session()) {
m_rootAudioRenderer = attached->session()->rootAudioRenderer();
} else {
qmlWarning(this) << "AudioRenderer could not find renderSession in context";
emit qmlEngine(this)->exit(1);
}
}
return m_rootAudioRenderer;
}

Expand Down Expand Up @@ -65,9 +75,6 @@ void AudioRenderer::setVolume(float volume)
void AudioRenderer::setUpstreamRenderer(AudioRenderer* upstreamRenderer)
{
if (upstreamRenderer != m_upstreamRenderer) {
// Don't allow it to be set on the root renderer
if (this == rootAudioRenderer())
return;
// Avoid circular dependencies
auto renderer = upstreamRenderer;
while (renderer != nullptr) {
Expand All @@ -77,26 +84,23 @@ void AudioRenderer::setUpstreamRenderer(AudioRenderer* upstreamRenderer)
}
renderer = renderer->upstreamRenderer();
}
upstreamRendererInternal()->removeDownstreamRenderer(this);
if (m_upstreamRenderer)
m_upstreamRenderer->removeDownstreamRenderer(this);
m_upstreamRenderer = upstreamRenderer;
upstreamRendererInternal()->addDownstreamRenderer(this);
if (m_upstreamRenderer)
m_upstreamRenderer->addDownstreamRenderer(this);
emit upstreamRendererChanged();
}
}

AudioRenderer* AudioRenderer::upstreamRendererInternal()
{
return m_upstreamRenderer ? m_upstreamRenderer : rootAudioRenderer();
}

void AudioRenderer::addDownstreamRenderer(AudioRenderer* parent)
void AudioRenderer::addDownstreamRenderer(AudioRenderer* downstreamRenderer)
{
m_downstreamRenderers.append(parent);
m_downstreamRenderers.append(downstreamRenderer);
}

void AudioRenderer::removeDownstreamRenderer(AudioRenderer* parent)
void AudioRenderer::removeDownstreamRenderer(AudioRenderer* downstreamRenderer)
{
m_downstreamRenderers.removeAll(parent);
m_downstreamRenderers.removeAll(downstreamRenderer);
}

void AudioRenderer::addAudioBuffer(QAudioBuffer audioBuffer)
Expand All @@ -112,9 +116,9 @@ QAudioBuffer AudioRenderer::mix()
return QAudioBuffer();
}

// Mix each parent and add their valid buffers
for (auto parent : m_downstreamRenderers) {
QAudioBuffer buffer = parent->mix();
// Mix each downstream and add their valid buffers
for (auto downstream : m_downstreamRenderers) {
QAudioBuffer buffer = downstream->mix();
if (buffer.isValid())
audioBuffers.append(buffer);
}
Expand Down
9 changes: 4 additions & 5 deletions src/MediaFX/audio_renderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,15 @@ class AudioRenderer : public QObject, public QQmlParserStatus {
QAudioBuffer mix();

protected:
void classBegin() override;
void componentComplete() override {};
void classBegin() override {};
void componentComplete() override;

private:
Q_DISABLE_COPY(AudioRenderer);

AudioRenderer* rootAudioRenderer();
AudioRenderer* upstreamRendererInternal();
void addDownstreamRenderer(AudioRenderer* parent);
void removeDownstreamRenderer(AudioRenderer* parent);
void addDownstreamRenderer(AudioRenderer* downstreamRenderer);
void removeDownstreamRenderer(AudioRenderer* downstreamRenderer);

float m_volume = 1.0;
QList<QAudioBuffer> audioBuffers;
Expand Down
48 changes: 25 additions & 23 deletions src/MediaFX/encoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,26 @@ Encoder::~Encoder()
avformat_free_context(m_formatContext);
}

void Encoder::setOutputFileName(const QString& outputFileName)
void Encoder::setRenderContext(const RenderContext* renderContext)
{
if (!m_outputFileName.isNull()) {
qmlWarning(this) << "Encoder outputFileName is a write-once property and cannot be changed";
if (m_renderContext) {
qmlWarning(this) << "Encoder renderContext is a write-once property and cannot be changed";
return;
}
if (m_outputFileName != outputFileName) {
m_outputFileName = outputFileName;
emit outputFileNameChanged();
if (renderContext != m_renderContext) {
m_renderContext = renderContext;
initialize(m_renderContext);
emit renderContextChanged();
}
}

void Encoder::initialize(const RenderContext& renderContext)
void Encoder::initialize(const RenderContext* renderContext)
{
int ret = 0;

// Select nut format
if ((ret = avformat_alloc_output_context2(&m_formatContext, nullptr, "nut", qUtf8Printable(m_outputFileName))) < 0) {
qCritical() << "Could not allocate an output context, error:" << av_err2qstring(ret);
if ((ret = avformat_alloc_output_context2(&m_formatContext, nullptr, "nut", qUtf8Printable(renderContext->outputFileName()))) < 0) {
qmlWarning(this) << "Could not allocate an output context, error:" << av_err2qstring(ret);
return;
}

Expand All @@ -81,9 +83,9 @@ void Encoder::initialize(const RenderContext& renderContext)
return;
AVCodecContext* videoCodecContext = video->codecContext();
videoCodecContext->pix_fmt = VideoPixelFormat_FFMPEG;
videoCodecContext->width = renderContext.frameSize().width();
videoCodecContext->height = renderContext.frameSize().height();
AVRational timeBase(av_inv_q(renderContext.frameRate()));
videoCodecContext->width = renderContext->data().frameSize().width();
videoCodecContext->height = renderContext->data().frameSize().height();
AVRational timeBase(av_inv_q(renderContext->data().frameRate()));
int64_t gcd = av_gcd(FFABS(timeBase.num), FFABS(timeBase.den));
if (gcd) {
timeBase.num = FFABS(timeBase.num) / gcd;
Expand All @@ -99,13 +101,13 @@ void Encoder::initialize(const RenderContext& renderContext)
return;
AVCodecContext* audioCodecContext = audio->codecContext();
audioCodecContext->sample_fmt = AudioSampleFormat_FFMPEG;
audioCodecContext->sample_rate = renderContext.sampleRate();
audioCodecContext->sample_rate = renderContext->data().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);
#else
if ((ret = av_channel_layout_copy(&audioCodecContext->ch_layout, &AudioChannelLayout_FFMPEG)) < 0) {
qCritical() << "Could not copy channel layout, error:" << av_err2qstring(ret);
qmlWarning(this) << "Could not copy channel layout, error:" << av_err2qstring(ret);
return;
}
#endif
Expand All @@ -117,35 +119,35 @@ void Encoder::initialize(const RenderContext& renderContext)
m_videoStream.swap(video);

if (!(m_formatContext->flags & AVFMT_NOFILE)) {
if ((ret = avio_open(&m_formatContext->pb, qUtf8Printable(m_outputFileName), AVIO_FLAG_WRITE)) < 0) {
qCritical() << "Could not open output file" << m_outputFileName << ", avio_open:" << av_err2qstring(ret);
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);
return;
}
}
AVDictionary* opt = nullptr;
if ((ret = av_dict_set(&opt, "fflags", "bitexact", 0)) < 0) {
qCritical() << "Could not set options, av_dict_set:" << av_err2qstring(ret);
qmlWarning(this) << "Could not set options, av_dict_set:" << av_err2qstring(ret);
return;
}
ret = avformat_write_header(m_formatContext, &opt);
av_dict_free(&opt);
if (ret < 0) {
qCritical() << "Could not open output file, avio_open:" << av_err2qstring(ret);
qmlWarning(this) << "Could not open output file, avio_open:" << av_err2qstring(ret);
return;
}

m_isValid = true;
}

void Encoder::componentComplete()
{
const RenderContext& renderContext = qmlEngine(this)->singletonInstance<RenderSession*>("MediaFX", "RenderSession")->renderContext();
initialize(renderContext);
if (!m_renderContext) {
qmlWarning(this) << "Encoder renderContext is a required property";
emit qmlEngine(this)->exit(1);
}
}

bool Encoder::encode(const QAudioBuffer& audioBuffer, const QByteArray& videoData)
{
if (!m_isValid) {
if (!m_renderContext) {
emit encodingError();
return false;
}
Expand Down
16 changes: 9 additions & 7 deletions src/MediaFX/encoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <chrono>
#include <memory>
Q_MOC_INCLUDE("output_stream.h")
Q_MOC_INCLUDE("render_context.h")
class RenderContext;
class OutputStream;
class QAudioBuffer;
Expand All @@ -20,7 +21,7 @@ using namespace std::chrono;
class Encoder : public QObject, public QQmlParserStatus {
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
Q_PROPERTY(QString outputFileName READ outputFileName WRITE setOutputFileName NOTIFY outputFileNameChanged FINAL)
Q_PROPERTY(const RenderContext* renderContext READ renderContext WRITE setRenderContext NOTIFY renderContextChanged FINAL REQUIRED)
QML_ELEMENT

public:
Expand All @@ -31,26 +32,27 @@ class Encoder : public QObject, public QQmlParserStatus {
Encoder& operator=(Encoder&&) = delete;
~Encoder() override;

void initialize(const RenderContext& renderContext);
const QString& outputFileName() const { return m_outputFileName; }
void setOutputFileName(const QString& outputFileName);
const RenderContext* renderContext() const { return m_renderContext; }
void setRenderContext(const RenderContext*);

signals:
void outputFileNameChanged();
void renderContextChanged();
void encodingError();

public slots:
bool encode(const QAudioBuffer& audioBuffer, const QByteArray& videoData);
bool finish();

protected:
void classBegin() override { }
void classBegin() override {};
void componentComplete() override;

private:
Q_DISABLE_COPY(Encoder);

bool m_isValid = false;
void initialize(const RenderContext* renderContext);

const RenderContext* m_renderContext = nullptr;
QString m_outputFileName;
AVFormatContext* m_formatContext = nullptr;
std::unique_ptr<OutputStream> m_videoStream;
Expand Down
17 changes: 10 additions & 7 deletions src/MediaFX/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,18 @@ int encoder(QGuiApplication& app, QCommandLineParser& parser)
output = u"pipe:"_s;

QQmlApplicationEngine engine;
RenderContext renderContext(url, output, frameSize, frameRate, sampleRate);
RenderSession* renderSession = engine.singletonInstance<RenderSession*>("MediaFX", "RenderSession");
Q_ASSERT(renderSession);
renderSession->initialize(renderContext);

RenderContextData renderContextData(url, output, frameSize, frameRate, sampleRate);
RenderContext* renderContext = engine.singletonInstance<RenderContext*>("MediaFX", "RenderContext");
Q_ASSERT(renderContext);
renderContext->setData(renderContextData);

auto fatalExit = [&engine]() {
emit engine.exit(1);
};
if (parser.isSet(u"exitOnWarning"_s)) {
QObject::connect(&engine, &QQmlApplicationEngine::warnings, renderSession, &RenderSession::fatalError, Qt::QueuedConnection);
QObject::connect(&engine, &QQmlApplicationEngine::warnings, &engine, fatalExit, Qt::QueuedConnection);
}
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, renderSession, &RenderSession::fatalError, Qt::QueuedConnection);
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &engine, fatalExit, Qt::QueuedConnection);
engine.load(QUrl(u"qrc:/qt/qml/MediaFX/app-encoder.qml"_s));

return app.exec();
Expand Down
Loading

0 comments on commit 8631802

Please sign in to comment.