From 8631802d8e435b4bca4571f507c8495d09dfaf01 Mon Sep 17 00:00:00 2001 From: Andrew Wason Date: Mon, 22 Apr 2024 14:39:39 -0400 Subject: [PATCH] Make session available via attached property. Working. --- README.md | 4 +-- src/MediaFX/app-encoder.qml | 47 +++++++++++++++++------------- src/MediaFX/audio_renderer.cpp | 50 +++++++++++++++++--------------- src/MediaFX/audio_renderer.h | 9 +++--- src/MediaFX/encoder.cpp | 48 +++++++++++++++--------------- src/MediaFX/encoder.h | 16 +++++----- src/MediaFX/main.cpp | 17 ++++++----- src/MediaFX/media_clip.cpp | 15 +++++++--- src/MediaFX/render_context.cpp | 7 ++++- src/MediaFX/render_context.h | 45 +++++++++++++++++++--------- src/MediaFX/render_session.cpp | 50 ++++++++++++++++++++++++-------- src/MediaFX/render_session.h | 44 +++++++++++++++++++++++----- src/MediaFX/render_window.cpp | 14 ++++++--- src/MediaFX/render_window.h | 7 +++-- src/MediaFX/sequence.js | 4 +-- tests/qml/animated.qml | 2 +- tests/qml/async.qml | 8 +++-- tests/qml/demo.qml | 2 +- tests/qml/multisink.qml | 2 +- tests/qml/sequence.qml | 2 +- tests/qml/static.qml | 4 ++- tests/qml/video-ad-insertion.qml | 2 +- tests/qml/video-clipstart.qml | 2 +- tests/qml/video-multieffect.qml | 2 +- tests/qml/video-shadereffect.qml | 2 +- tests/tst_encoder.cpp | 25 ++++++++-------- 26 files changed, 273 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index 18a249e..3260e7e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Item { } Component.onCompleted: { - videoClip.clipEnded.connect(RenderSession.endSession); + videoClip.clipEnded.connect(RenderSession.session.endSession); } } VideoRenderer { @@ -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). diff --git a/src/MediaFX/app-encoder.qml b/src/MediaFX/app-encoder.qml index 221e8a4..adae416 100644 --- a/src/MediaFX/app-encoder.qml +++ b/src/MediaFX/app-encoder.qml @@ -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 } } diff --git a/src/MediaFX/audio_renderer.cpp b/src/MediaFX/audio_renderer.cpp index b8bc6d9..49cccf8 100644 --- a/src/MediaFX/audio_renderer.cpp +++ b/src/MediaFX/audio_renderer.cpp @@ -5,6 +5,7 @@ #include "render_session.h" #include #include +#include #include #include #include @@ -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("MediaFX", "RenderSession")->rootAudioRenderer(); + if (!m_rootAudioRenderer) { + RenderSessionAttached* attached = qobject_cast(qmlAttachedPropertiesObject(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; } @@ -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) { @@ -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) @@ -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); } diff --git a/src/MediaFX/audio_renderer.h b/src/MediaFX/audio_renderer.h index efe70ec..e4d321d 100644 --- a/src/MediaFX/audio_renderer.h +++ b/src/MediaFX/audio_renderer.h @@ -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 audioBuffers; diff --git a/src/MediaFX/encoder.cpp b/src/MediaFX/encoder.cpp index dfc98c5..fcae9c4 100644 --- a/src/MediaFX/encoder.cpp +++ b/src/MediaFX/encoder.cpp @@ -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; } @@ -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; @@ -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 @@ -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("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; } diff --git a/src/MediaFX/encoder.h b/src/MediaFX/encoder.h index 9a8cef9..c7c40a9 100644 --- a/src/MediaFX/encoder.h +++ b/src/MediaFX/encoder.h @@ -11,6 +11,7 @@ #include #include Q_MOC_INCLUDE("output_stream.h") +Q_MOC_INCLUDE("render_context.h") class RenderContext; class OutputStream; class QAudioBuffer; @@ -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: @@ -31,12 +32,11 @@ 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: @@ -44,13 +44,15 @@ public slots: 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 m_videoStream; diff --git a/src/MediaFX/main.cpp b/src/MediaFX/main.cpp index ccdb13e..c223323 100644 --- a/src/MediaFX/main.cpp +++ b/src/MediaFX/main.cpp @@ -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("MediaFX", "RenderSession"); - Q_ASSERT(renderSession); - renderSession->initialize(renderContext); - + RenderContextData renderContextData(url, output, frameSize, frameRate, sampleRate); + RenderContext* renderContext = engine.singletonInstance("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(); diff --git a/src/MediaFX/media_clip.cpp b/src/MediaFX/media_clip.cpp index a7a2557..7e445bc 100644 --- a/src/MediaFX/media_clip.cpp +++ b/src/MediaFX/media_clip.cpp @@ -7,6 +7,7 @@ #include "render_session.h" #include "util.h" #include +#include #include #include #include @@ -205,10 +206,16 @@ void MediaClip::loadMedia() void MediaClip::classBegin() { - m_renderSession = qmlEngine(this)->singletonInstance("MediaFX", "RenderSession"); - connect( - m_renderSession, &RenderSession::renderMediaClips, - this, &MediaClip::render); + RenderSessionAttached* attached = qobject_cast(qmlAttachedPropertiesObject(this)); + if (attached && attached->session()) { + m_renderSession = attached->session(); + connect( + m_renderSession, &RenderSession::renderMediaClips, + this, &MediaClip::render); + } else { + qmlWarning(this) << "MediaClip could not find renderSession in context"; + emit qmlEngine(this)->exit(1); + } } void MediaClip::componentComplete() diff --git a/src/MediaFX/render_context.cpp b/src/MediaFX/render_context.cpp index 6636a7b..79cef79 100644 --- a/src/MediaFX/render_context.cpp +++ b/src/MediaFX/render_context.cpp @@ -5,11 +5,16 @@ extern "C" { #include } -RenderContext::RenderContext(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 AVRational& frameRate, int sampleRate) : m_sourceUrl(sourceUrl) , m_outputFileName(outputFileName) , m_frameSize(frameSize) , m_frameRate(frameRate) , m_sampleRate(sampleRate) { +} + +RenderContext::RenderContext(QObject* parent) + : QObject(parent) +{ } \ No newline at end of file diff --git a/src/MediaFX/render_context.h b/src/MediaFX/render_context.h index 36ec6bb..fdcea68 100644 --- a/src/MediaFX/render_context.h +++ b/src/MediaFX/render_context.h @@ -12,22 +12,16 @@ extern "C" { #include } -class RenderContext { - Q_GADGET - Q_PROPERTY(QUrl sourceUrl READ sourceUrl CONSTANT) - Q_PROPERTY(QString outputFileName READ outputFileName CONSTANT) - Q_PROPERTY(QSize frameSize READ frameSize CONSTANT) - QML_UNCREATABLE("") - QML_VALUE_TYPE(renderContext) +class RenderContextData { public: // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - explicit RenderContext(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 AVRational& frameRate = { 30, 1 }, int sampleRate = 44100); - RenderContext(RenderContext&&) = default; - RenderContext(const RenderContext&) = default; - RenderContext& operator=(RenderContext&&) = default; - RenderContext& operator=(const RenderContext&) = default; - ~RenderContext() = default; + RenderContextData(RenderContextData&&) = default; + RenderContextData(const RenderContextData&) = default; + RenderContextData& operator=(RenderContextData&&) = default; + RenderContextData& operator=(const RenderContextData&) = default; + ~RenderContextData() = default; constexpr const QUrl& sourceUrl() const { return m_sourceUrl; } constexpr const QString& outputFileName() const { return m_outputFileName; } @@ -42,3 +36,28 @@ class RenderContext { QUrl m_sourceUrl; QString m_outputFileName; }; + +class RenderContext : public QObject { + Q_OBJECT + Q_PROPERTY(QUrl sourceUrl READ sourceUrl CONSTANT) + Q_PROPERTY(QString outputFileName READ outputFileName CONSTANT) + Q_PROPERTY(QSize frameSize READ frameSize CONSTANT) + QML_ELEMENT + QML_SINGLETON +public: + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + explicit RenderContext(QObject* parent = nullptr); + RenderContext(RenderContext&&) = delete; + RenderContext& operator=(RenderContext&&) = delete; + ~RenderContext() override = default; + + constexpr const QUrl& sourceUrl() const { return m_data.sourceUrl(); } + constexpr const QString& outputFileName() const { return m_data.outputFileName(); } + constexpr const QSize& frameSize() const noexcept { return m_data.frameSize(); } + constexpr const RenderContextData& data() const noexcept { return m_data; } + void setData(const RenderContextData& data) { m_data = data; } + +private: + Q_DISABLE_COPY(RenderContext); + RenderContextData m_data; +}; diff --git a/src/MediaFX/render_session.cpp b/src/MediaFX/render_session.cpp index 0818098..9056ea1 100644 --- a/src/MediaFX/render_session.cpp +++ b/src/MediaFX/render_session.cpp @@ -12,8 +12,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -22,11 +24,12 @@ \qmltype RenderSession //! \instantiates RenderSession \inqmlmodule MediaFX - \brief The RenderSession singleton manages the current rendering session. + \brief The RenderSession manages the current rendering session. Internally, RenderSession drives rendering the set of active MediaClips. It also exposes access to the current rendering time and allows the session to be shut down, see \l {RenderSession::endSession}. + The current session can be accessed using the \l {RenderSession::session} attached property. XXX */ /*! @@ -60,23 +63,46 @@ RenderSession::~RenderSession() m_animationDriver->uninstall(); } -void RenderSession::initialize(const RenderContext& renderContext) +/*! + \qmlattachedproperty RenderSession RenderSession::session + This property holds the current RenderSession. + This allows the session to be accessed from any QML object. +*/ +RenderSessionAttached* RenderSession::qmlAttachedProperties(QObject* object) { - if (m_renderContext) { - qCritical() << "RenderSession is already initialized"; - emit qmlEngine(this)->exit(1); - return; + // Loader sets a renderSession property on the context the qml is loaded in + RenderSession* session = QQmlEngine::contextForObject(object)->contextProperty(u"renderSession"_s).value(); + if (session) + return new RenderSessionAttached(session, object); + else + return nullptr; +} + +QQmlListProperty RenderSession::data() +{ + return QQmlListProperty(this, nullptr, &RenderSession::append_data, nullptr, nullptr, nullptr, nullptr, nullptr); +} + +void RenderSession::append_data(QQmlListProperty* list, QObject* object) +{ + RenderSession* session = qobject_cast(list->object); + if (session) { + session->m_data.append(object); } - m_renderContext = std::make_unique(renderContext); - m_currentRenderTime = Interval(0us, frameRateToFrameDuration(m_renderContext->frameRate())); +} + +void RenderSession::classBegin() +{ + m_renderContext = qmlEngine(this)->singletonInstance("MediaFX", "RenderContext"); + m_currentRenderTime = Interval(0us, frameRateToFrameDuration(m_renderContext->data().frameRate())); m_rootAudioRenderer = std::make_unique(); - m_animationDriver = std::make_unique(frameRateToFrameDuration(m_renderContext->frameRate())); + m_animationDriver = std::make_unique(frameRateToFrameDuration(m_renderContext->data().frameRate())); m_animationDriver->install(); m_outputAudioFormat.setSampleFormat(AudioSampleFormat_Qt); m_outputAudioFormat.setChannelConfig(AudioChannelLayout_Qt); - m_outputAudioFormat.setSampleRate(m_renderContext->sampleRate()); + m_outputAudioFormat.setSampleRate(m_renderContext->data().sampleRate()); } /*! @@ -122,7 +148,7 @@ void RenderSession::render() emit renderScene(); m_frameCount++; - m_currentRenderTime = m_currentRenderTime.nextInterval(duration_cast(m_frameCount * frameRateToFrameDuration(renderContext().frameRate()))); + m_currentRenderTime = m_currentRenderTime.nextInterval(duration_cast(m_frameCount * frameRateToFrameDuration(renderContext()->data().frameRate()))); emit currentRenderTimeChanged(); if (isSessionEnded()) { @@ -194,7 +220,7 @@ bool RenderSession::event(QEvent* event) const QAudioBuffer& RenderSession::silentOutputAudioBuffer() { if (!m_silentOutputAudioBuffer.isValid()) { - m_silentOutputAudioBuffer = QAudioBuffer(m_outputAudioFormat.framesForDuration(frameRateToFrameDuration(renderContext().frameRate()).count()), m_outputAudioFormat); + m_silentOutputAudioBuffer = QAudioBuffer(m_outputAudioFormat.framesForDuration(frameRateToFrameDuration(renderContext()->data().frameRate()).count()), m_outputAudioFormat); } return m_silentOutputAudioBuffer; } diff --git a/src/MediaFX/render_session.h b/src/MediaFX/render_session.h index f6df4e8..733e749 100644 --- a/src/MediaFX/render_session.h +++ b/src/MediaFX/render_session.h @@ -7,7 +7,10 @@ #include "render_context.h" #include #include +#include #include +#include +#include #include #include #include @@ -17,14 +20,18 @@ extern "C" { } class AnimationDriver; class AudioRenderer; +class RenderSessionAttached; using namespace std::chrono; -class RenderSession : public QObject { +class RenderSession : public QObject, public QQmlParserStatus { Q_OBJECT + Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(IntervalGadget currentRenderTime READ currentRenderTime NOTIFY currentRenderTimeChanged FINAL) - Q_PROPERTY(RenderContext renderContext READ renderContext CONSTANT) + Q_PROPERTY(const RenderContext* renderContext READ renderContext CONSTANT) + Q_PROPERTY(QQmlListProperty data READ data FINAL) + Q_CLASSINFO("DefaultProperty", "data") + QML_ATTACHED(RenderSessionAttached) QML_ELEMENT - QML_SINGLETON public: RenderSession(QObject* parent = nullptr); @@ -32,16 +39,17 @@ class RenderSession : public QObject { RenderSession& operator=(RenderSession&&) = delete; ~RenderSession() override; - void initialize(const RenderContext& renderContext); - - const RenderContext& renderContext() const { return *m_renderContext.get(); } + static RenderSessionAttached* qmlAttachedProperties(QObject* object); + const RenderContext* renderContext() const { return m_renderContext; } Q_INVOKABLE IntervalGadget createInterval(qint64 start, qint64 end) const { return IntervalGadget(start, end); }; - const AVRational& outputFrameRate() const { return renderContext().frameRate(); } + QQmlListProperty data(); + + const AVRational& outputFrameRate() const { return renderContext()->data().frameRate(); } const QAudioFormat& outputAudioFormat() const { return m_outputAudioFormat; } const IntervalGadget currentRenderTime() const { return IntervalGadget(m_currentRenderTime); } @@ -57,6 +65,7 @@ class RenderSession : public QObject { bool isSessionEnded() const { return m_sessionEnded; } signals: + void dataChanged(); void currentRenderTimeChanged(); void sessionEnded(); void renderMediaClips(); @@ -69,11 +78,16 @@ public slots: protected: void postRenderEvent(); + void classBegin() override; + void componentComplete() override { } private: Q_DISABLE_COPY(RenderSession); - std::unique_ptr m_renderContext; + static void append_data(QQmlListProperty* list, QObject* object); + + QList m_data; + const RenderContext* m_renderContext = nullptr; QAudioFormat m_outputAudioFormat; Interval m_currentRenderTime; int m_frameCount = 1; @@ -85,3 +99,17 @@ public slots: bool m_isRenderEventPosted = false; bool m_isResumingRender = false; }; + +class RenderSessionAttached : public QObject { + Q_OBJECT + Q_PROPERTY(RenderSession* session READ session CONSTANT) + QML_ANONYMOUS +public: + explicit RenderSessionAttached(RenderSession* session, QObject* parent = nullptr) + : QObject(parent) + , m_session(session) {}; + RenderSession* session() const { return m_session; }; + +private: + RenderSession* m_session = nullptr; +}; \ No newline at end of file diff --git a/src/MediaFX/render_window.cpp b/src/MediaFX/render_window.cpp index d84f210..ae87dae 100644 --- a/src/MediaFX/render_window.cpp +++ b/src/MediaFX/render_window.cpp @@ -59,13 +59,19 @@ void RenderWindow::componentComplete() // QQuickWindow does not resize contentItem // https://bugreports.qt.io/browse/QTBUG-55028 contentItem()->setSize(size()); + + if (!m_renderSession) { + qmlWarning(this) << "RenderWindow renderSession is a required property"; + qmlEngine(this)->exit(1); + } } -RenderSession* RenderWindow::renderSession() +void RenderWindow::setRenderSession(RenderSession* renderSession) { - if (!m_renderSession) - m_renderSession = qmlEngine(this)->singletonInstance("MediaFX", "RenderSession"); - return m_renderSession; + if (renderSession != m_renderSession) { + m_renderSession = renderSession; + emit renderSessionChanged(); + } } void RenderWindow::render() diff --git a/src/MediaFX/render_window.h b/src/MediaFX/render_window.h index 9a475c2..32d542b 100644 --- a/src/MediaFX/render_window.h +++ b/src/MediaFX/render_window.h @@ -23,6 +23,7 @@ class RenderSession; class RenderWindow : public QQuickWindow, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) + Q_PROPERTY(RenderSession* renderSession READ renderSession WRITE setRenderSession NOTIFY renderSessionChanged REQUIRED FINAL) QML_ELEMENT public: @@ -33,7 +34,11 @@ class RenderWindow : public QQuickWindow, public QQmlParserStatus { RenderWindow& operator=(RenderWindow&&) = delete; ~RenderWindow() override; + RenderSession* renderSession() const { return m_renderSession; } + void setRenderSession(RenderSession* renderSession); + signals: + void renderSessionChanged(); void frameReady(const QAudioBuffer& audioBuffer, const QByteArray& videoData); public slots: @@ -48,8 +53,6 @@ public slots: RenderWindow(RenderControl* renderControl); - RenderSession* renderSession(); - RenderSession* m_renderSession = nullptr; #ifdef MEDIAFX_ENABLE_VULKAN QVulkanInstance m_vulkanInstance; diff --git a/src/MediaFX/sequence.js b/src/MediaFX/sequence.js index 27a0345..7c07113 100644 --- a/src/MediaFX/sequence.js +++ b/src/MediaFX/sequence.js @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later function onClipEnded() { - RenderSession.currentRenderTimeChanged.connect(nextClip); + root.RenderSession.session.currentRenderTimeChanged.connect(nextClip); }; function onCurrentFrameTimechanged() { @@ -14,7 +14,7 @@ function onCurrentFrameTimechanged() { }; function nextClip() { - RenderSession.currentRenderTimeChanged.disconnect(nextClip); + root.RenderSession.session.currentRenderTimeChanged.disconnect(nextClip); if (internal.currentClipIndex + 1 < root.mediaClips.length) { const clip = root.mediaClips[internal.currentClipIndex]; clip.currentFrameTimeChanged.disconnect(onCurrentFrameTimechanged); diff --git a/tests/qml/animated.qml b/tests/qml/animated.qml index 00748e5..6e94919 100644 --- a/tests/qml/animated.qml +++ b/tests/qml/animated.qml @@ -19,7 +19,7 @@ Item { transitions: Transition { onRunningChanged: { if (!running) - RenderSession.endSession(); + container.RenderSession.session.endSession(); } AnchorAnimation { diff --git a/tests/qml/async.qml b/tests/qml/async.qml index c90952a..d75ade4 100644 --- a/tests/qml/async.qml +++ b/tests/qml/async.qml @@ -6,6 +6,8 @@ import QtQuick.Layouts import MediaFX ColumnLayout { + id: root + spacing: 0 AudioRenderer { @@ -19,11 +21,11 @@ ColumnLayout { source: Qt.resolvedUrl("../fixtures/assets/ednotsafe-320x180-15fps-1.53s-44100.nut") function resumeRendering() { - RenderSession.resumeRendering(); + root.RenderSession.session.resumeRendering(); } onCurrentFrameTimeChanged: function () { if (videoClipA.currentFrameTime.contains(500)) { - RenderSession.pauseRendering(); + root.RenderSession.session.pauseRendering(); Qt.callLater(resumeRendering); } } @@ -47,6 +49,6 @@ ColumnLayout { } Component.onCompleted: { - videoClipA.clipEnded.connect(RenderSession.endSession); + videoClipA.clipEnded.connect(root.RenderSession.session.endSession); } } diff --git a/tests/qml/demo.qml b/tests/qml/demo.qml index e5f140d..6d0db98 100644 --- a/tests/qml/demo.qml +++ b/tests/qml/demo.qml @@ -14,7 +14,7 @@ Item { source: Qt.resolvedUrl("../fixtures/assets/red-320x180-15fps-8s-kal1624000.nut") Component.onCompleted: { - videoClip.clipEnded.connect(RenderSession.endSession); + videoClip.clipEnded.connect(videoClip.RenderSession.session.endSession); } } VideoRenderer { diff --git a/tests/qml/multisink.qml b/tests/qml/multisink.qml index de1b1ad..38c8367 100644 --- a/tests/qml/multisink.qml +++ b/tests/qml/multisink.qml @@ -12,7 +12,7 @@ Item { source: Qt.resolvedUrl("../fixtures/assets/red-640x360-30fps-4s-rms44100.nut") Component.onCompleted: { - videoClip.clipEnded.connect(RenderSession.endSession); + videoClip.clipEnded.connect(root.RenderSession.session.endSession); } } MediaClip { diff --git a/tests/qml/sequence.qml b/tests/qml/sequence.qml index b4f2a58..9f21732 100644 --- a/tests/qml/sequence.qml +++ b/tests/qml/sequence.qml @@ -41,7 +41,7 @@ MediaSequence { ] Component.onCompleted: { - sequence.mediaSequenceEnded.connect(RenderSession.endSession); + sequence.mediaSequenceEnded.connect(sequence.RenderSession.session.endSession); } MediaClip { diff --git a/tests/qml/static.qml b/tests/qml/static.qml index 44f4c4d..f531161 100644 --- a/tests/qml/static.qml +++ b/tests/qml/static.qml @@ -5,7 +5,9 @@ import QtQuick import MediaFX Rectangle { + id: root + color: "red" - Component.onCompleted: RenderSession.endSession() + Component.onCompleted: root.RenderSession.session.endSession() } diff --git a/tests/qml/video-ad-insertion.qml b/tests/qml/video-ad-insertion.qml index 22bd03a..226c915 100644 --- a/tests/qml/video-ad-insertion.qml +++ b/tests/qml/video-ad-insertion.qml @@ -16,7 +16,7 @@ Item { Component.onCompleted: { // End encoding when main videoClip finishes - videoClip.clipEnded.connect(RenderSession.endSession); + videoClip.clipEnded.connect(videoClip.RenderSession.session.endSession); } } MediaClip { diff --git a/tests/qml/video-clipstart.qml b/tests/qml/video-clipstart.qml index 8ea4d4c..23b94b6 100644 --- a/tests/qml/video-clipstart.qml +++ b/tests/qml/video-clipstart.qml @@ -12,7 +12,7 @@ Item { source: Qt.resolvedUrl("../fixtures/assets/red-320x180-15fps-8s-kal1624000.nut") Component.onCompleted: { - videoClip.clipEnded.connect(RenderSession.endSession); + videoClip.clipEnded.connect(videoClip.RenderSession.session.endSession); } } VideoRenderer { diff --git a/tests/qml/video-multieffect.qml b/tests/qml/video-multieffect.qml index 9ddfc9e..e603b73 100644 --- a/tests/qml/video-multieffect.qml +++ b/tests/qml/video-multieffect.qml @@ -12,7 +12,7 @@ Item { source: Qt.resolvedUrl("../fixtures/assets/blue-320x180-30fps-3s-awb44100.nut") Component.onCompleted: { - videoClip.clipEnded.connect(RenderSession.endSession); + videoClip.clipEnded.connect(videoClip.RenderSession.session.endSession); } } VideoRenderer { diff --git a/tests/qml/video-shadereffect.qml b/tests/qml/video-shadereffect.qml index a6fb1fb..5f11b48 100644 --- a/tests/qml/video-shadereffect.qml +++ b/tests/qml/video-shadereffect.qml @@ -11,7 +11,7 @@ Item { source: Qt.resolvedUrl("../fixtures/assets/blue-320x180-30fps-3s-awb44100.nut") Component.onCompleted: { - videoClip.clipEnded.connect(RenderSession.endSession); + videoClip.clipEnded.connect(videoClip.RenderSession.session.endSession); } } VideoRenderer { diff --git a/tests/tst_encoder.cpp b/tests/tst_encoder.cpp index 858243a..be4e323 100644 --- a/tests/tst_encoder.cpp +++ b/tests/tst_encoder.cpp @@ -49,25 +49,26 @@ private slots: QFile encodedFile(outputDir.filePath("encoder.nut")); - const RenderContext renderContext(QUrl(), encodedFile.fileName(), QSize(160, 120), AVRational { 5, 1 }, 44100); + 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.setOutputFileName(renderContext.outputFileName()); - encoder.initialize(renderContext); + encoder.setRenderContext(&renderContext); QAudioFormat audioFormat; audioFormat.setSampleFormat(QAudioFormat::Float); audioFormat.setChannelConfig(QAudioFormat::ChannelConfigStereo); - audioFormat.setSampleRate(renderContext.sampleRate()); - QAudioBuffer audioBuffer(audioFormat.framesForDuration(frameRateToFrameDuration(renderContext.frameRate()).count()), audioFormat); + audioFormat.setSampleRate(renderContext.data().sampleRate()); + QAudioBuffer audioBuffer(audioFormat.framesForDuration(frameRateToFrameDuration(renderContext.data().frameRate()).count()), audioFormat); - QByteArray videoData(static_cast(renderContext.frameSize().width() * renderContext.frameSize().height() * 4), Qt::Uninitialized); + QByteArray videoData(static_cast(renderContext.data().frameSize().width() * renderContext.data().frameSize().height() * 4), Qt::Uninitialized); - const int frames = static_cast(2.0 * av_q2d(renderContext.frameRate())); // 2 seconds + const int frames = static_cast(2.0 * av_q2d(renderContext.data().frameRate())); // 2 seconds double audioTime = 0; - double audioIncr = 2 * M_PI * 110.0 / renderContext.sampleRate(); - double audioIncr2 = 2 * M_PI * 110.0 / renderContext.sampleRate() / renderContext.sampleRate(); + double audioIncr = 2 * M_PI * 110.0 / renderContext.data().sampleRate(); + double audioIncr2 = 2 * M_PI * 110.0 / renderContext.data().sampleRate() / renderContext.data().sampleRate(); for (int i = 0; i < frames; i++) { // Audio sine wave @@ -80,9 +81,9 @@ private slots: audioIncr += audioIncr2; } // Video RGBA colors - int step = static_cast(av_q2d(renderContext.frameRate()) * i); - for (int y = 0; y < renderContext.frameSize().height(); y++) { - for (int x = 0; x < renderContext.frameSize().width(); x++) { + 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++) { // 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)]));