diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9117a31..77189eb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -117,7 +117,6 @@ jobs:
with:
clang-format-version: '15'
- name: qmlformat
- if: ${{ false }}
run: builders/Linux/docker-run.sh bash -c 'cd /mediafx && qmlformat -i $(git ls-files "**/*.qml") && git diff --exit-code'
- name: qmllint
if: ${{ false }}
diff --git a/.qmlformat.ini b/.qmlformat.ini
index 78963bc..f73a37c 100644
--- a/.qmlformat.ini
+++ b/.qmlformat.ini
@@ -2,6 +2,6 @@
FunctionsSpacing=
IndentWidth=4
NewlineType=unix
-NormalizeOrder=1
+NormalizeOrder=
ObjectsSpacing=
UseTabs=
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e7bfcf7..6ed0f0c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,6 +20,7 @@ project(mediaFX VERSION 1.0.0 LANGUAGES CXX)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Multimedia Qml Quick ShaderTools QUIET OPTIONAL_COMPONENTS Quick3D WebEngineQuick)
qt_standard_project_setup()
+qt_policy(SET QTP0001 NEW)
enable_testing(true)
set(CMAKE_CXX_STANDARD 20)
@@ -38,3 +39,4 @@ endif()
add_subdirectory(src/MediaFX)
add_subdirectory(tests)
+add_subdirectory(tools/viewer)
\ No newline at end of file
diff --git a/src/MediaFX/CMakeLists.txt b/src/MediaFX/CMakeLists.txt
index 466a0b0..d257152 100644
--- a/src/MediaFX/CMakeLists.txt
+++ b/src/MediaFX/CMakeLists.txt
@@ -13,14 +13,13 @@
# You should have received a copy of the GNU General Public License along with mediaFX.
# If not, see .
-qt_policy(SET QTP0001 NEW)
-
find_package(PkgConfig)
pkg_search_module(ffms2 REQUIRED IMPORTED_TARGET ffms2)
option(EVENT_LOGGER "Enable event logger" OFF)
qt_add_library(mediafx STATIC
+ application.cpp
session.cpp
media_manager.cpp
encoder.cpp
@@ -51,7 +50,22 @@ set_property(TARGET mediafxtool PROPERTY OUTPUT_NAME mediafx)
qt_add_qml_module(mediafx
URI MediaFX
QML_FILES
+ qml/sequence.js
+ qml/MediaSequence.qml
qml/MultiEffectState.qml
+ qml/ShaderEffectState.qml
+ qml/effects/MediaMixer.qml
+ qml/effects/CrossFadeMixer.qml
+ qml/effects/LumaMixer.qml
+ qml/effects/LumaGradientMixer.qml
+ qml/effects/WipeMixer.qml
+)
+qt_add_shaders(mediafx "shaders"
+ PREFIX
+ "/"
+ FILES
+ qml/effects/crossfade.frag
+ qml/effects/luma.frag
)
target_link_libraries(mediafx PUBLIC PkgConfig::ffms2 Qt6::Core Qt6::Gui Qt6::GuiPrivate Qt6::Multimedia Qt6::Qml Qt6::Quick)
diff --git a/src/MediaFX/application.cpp b/src/MediaFX/application.cpp
new file mode 100644
index 0000000..b0dcf45
--- /dev/null
+++ b/src/MediaFX/application.cpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 Andrew Wason
+ *
+ * This file is part of mediaFX.
+ *
+ * mediaFX is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with mediaFX.
+ * If not, see .
+ */
+
+#include "application.h"
+#include
+#ifdef WEBENGINEQUICK
+#include
+#endif
+
+void initializeMediaFX()
+{
+#ifdef WEBENGINEQUICK
+#if !defined(QT_NO_OPENGL)
+ // https://doc.qt.io/qt-6/qml-qtwebengine-webengineview.html#rendering-to-opengl-surface
+ // https://doc.qt.io/qt-6/qtwebengine-overview.html#embedding-web-content-into-qt-quick-applications
+ QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
+#endif
+ QtWebEngineQuick::initialize();
+#endif
+}
diff --git a/src/MediaFX/application.h b/src/MediaFX/application.h
new file mode 100644
index 0000000..5e16422
--- /dev/null
+++ b/src/MediaFX/application.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 Andrew Wason
+ *
+ * This file is part of mediaFX.
+ *
+ * mediaFX is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with mediaFX.
+ * If not, see .
+ */
+
+#pragma once
+
+#define qSL QStringLiteral
+
+void initializeMediaFX();
diff --git a/src/MediaFX/interval.h b/src/MediaFX/interval.h
index f6f98c2..42a60a5 100644
--- a/src/MediaFX/interval.h
+++ b/src/MediaFX/interval.h
@@ -19,6 +19,7 @@
#include
#include
+#include
#include
#include
#include
@@ -26,8 +27,10 @@ using namespace std::chrono;
class Interval {
Q_GADGET
+ QML_VALUE_TYPE(interval)
Q_PROPERTY(int start READ start FINAL)
Q_PROPERTY(int end READ end FINAL)
+ Q_PROPERTY(int duration READ duration FINAL)
public:
constexpr Interval() noexcept = default;
@@ -48,6 +51,7 @@ class Interval {
constexpr qint64 start() const noexcept { return duration_cast(s).count(); }
constexpr qint64 end() const noexcept { return duration_cast(e).count(); }
+ constexpr qint64 duration() const noexcept { return duration_cast(e - s).count(); }
Q_INVOKABLE constexpr bool contains(qint64 time) const noexcept
{
diff --git a/src/MediaFX/main.cpp b/src/MediaFX/main.cpp
index 76217eb..9a567f3 100644
--- a/src/MediaFX/main.cpp
+++ b/src/MediaFX/main.cpp
@@ -15,6 +15,7 @@
* If not, see .
*/
+#include "application.h"
#include "encoder.h"
#ifdef EVENTLOGGER
#include "event_logger.h"
@@ -28,12 +29,6 @@
#include
#include
-#ifdef WEBENGINEQUICK
-#include
-#endif
-
-#define qSL QStringLiteral
-
const auto ffmpegPreamble = qSL("-f rawvideo -video_size ${MEDIAFX_FRAMESIZE} -pixel_format rgb0 -framerate ${MEDIAFX_FRAMERATE} -i pipe:${MEDIAFX_VIDEOFD}");
int main(int argc, char* argv[])
@@ -43,15 +38,7 @@ int main(int argc, char* argv[])
const_cast("QT_MAC_DISABLE_FOREGROUND_APPLICATION_TRANSFORM=1"));
#endif
-#ifdef WEBENGINEQUICK
-#if !defined(QT_NO_OPENGL)
- // https://doc.qt.io/qt-6/qml-qtwebengine-webengineview.html#rendering-to-opengl-surface
- // https://doc.qt.io/qt-6/qtwebengine-overview.html#embedding-web-content-into-qt-quick-applications
- QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
-#endif
- QtWebEngineQuick::initialize();
-#endif
-
+ initializeMediaFX();
QGuiApplication app(argc, argv);
#ifdef EVENTLOGGER
diff --git a/src/MediaFX/media_clip.cpp b/src/MediaFX/media_clip.cpp
index 2059a97..19ac72a 100644
--- a/src/MediaFX/media_clip.cpp
+++ b/src/MediaFX/media_clip.cpp
@@ -65,6 +65,7 @@ void MediaClip::setClipStart(qint64 ms)
}
m_clipStart = ms;
emit clipStartChanged();
+ emit clipDurationChanged();
}
void MediaClip::setClipEnd(qint64 ms)
@@ -75,6 +76,7 @@ void MediaClip::setClipEnd(qint64 ms)
}
m_clipEnd = ms;
emit clipEndChanged();
+ emit clipDurationChanged();
}
void MediaClip::render()
@@ -82,6 +84,15 @@ void MediaClip::render()
if (!isActive())
return;
+ emit clipCurrentTimeChanged();
+
+ if (m_audioTrack)
+ m_audioTrack->render(m_currentFrameTime);
+ if (m_videoTrack)
+ m_videoTrack->render(m_currentFrameTime);
+
+ m_currentFrameTime = m_currentFrameTime.nextInterval(MediaManager::singletonInstance()->frameDuration());
+
if (m_currentFrameTime.start() >= m_clipEnd) {
if (m_audioTrack)
m_audioTrack->stop();
@@ -90,15 +101,6 @@ void MediaClip::render()
emit clipEnded();
return;
}
-
- emit clipCurrentTimeChanged();
-
- if (m_audioTrack)
- m_audioTrack->render(m_currentFrameTime);
- if (m_videoTrack)
- m_videoTrack->render(m_currentFrameTime);
-
- m_currentFrameTime = m_currentFrameTime.nextInterval(MediaManager::singletonInstance()->session()->frameDuration());
}
void MediaClip::setActive(bool active)
@@ -123,7 +125,7 @@ void MediaClip::loadMedia()
{
if (!source().isValid()) {
qmlWarning(this) << "MediaClip requires source Url";
- emit MediaManager::singletonInstance()->session()->exitApp(1);
+ emit MediaManager::singletonInstance()->window()->engine()->exit(1);
return;
}
ErrorInfo errorInfo;
@@ -132,14 +134,14 @@ void MediaClip::loadMedia()
FFMS_Indexer* indexer = FFMS_CreateIndexer(sourceFileUtf8.data(), &errorInfo);
if (!indexer) {
qmlWarning(this) << "MediaClip FFMS_CreateIndexer failed:" << errorInfo;
- emit MediaManager::singletonInstance()->session()->exitApp(1);
+ emit MediaManager::singletonInstance()->window()->engine()->exit(1);
return;
}
FFMS_Index* index = FFMS_DoIndexing2(indexer, FFMS_IEH_ABORT, &errorInfo);
if (!index) {
qmlWarning(this) << "MediaClip FFMS_DoIndexing2 failed:" << errorInfo;
- emit MediaManager::singletonInstance()->session()->exitApp(1);
+ emit MediaManager::singletonInstance()->window()->engine()->exit(1);
return;
}
@@ -167,5 +169,5 @@ void MediaClip::componentComplete()
if (clipEnd() < 0) {
setClipEnd(std::max(m_audioTrack ? m_audioTrack->duration() : 0, m_videoTrack ? m_videoTrack->duration() : 0));
}
- m_currentFrameTime = Interval(milliseconds(clipStart()), milliseconds(clipStart()) + MediaManager::singletonInstance()->session()->frameDuration());
+ m_currentFrameTime = Interval(milliseconds(clipStart()), milliseconds(clipStart()) + MediaManager::singletonInstance()->frameDuration());
}
\ No newline at end of file
diff --git a/src/MediaFX/media_clip.h b/src/MediaFX/media_clip.h
index 2198714..e511c65 100644
--- a/src/MediaFX/media_clip.h
+++ b/src/MediaFX/media_clip.h
@@ -33,6 +33,7 @@ class MediaClip : public QObject, public QQmlParserStatus {
Q_PROPERTY(QUrl source READ source WRITE setSource REQUIRED FINAL)
Q_PROPERTY(int clipStart READ clipStart WRITE setClipStart NOTIFY clipStartChanged FINAL)
Q_PROPERTY(int clipEnd READ clipEnd WRITE setClipEnd NOTIFY clipEndChanged FINAL)
+ Q_PROPERTY(int clipDuration READ clipDuration NOTIFY clipDurationChanged FINAL)
Q_PROPERTY(bool active READ isActive NOTIFY activeChanged FINAL)
Q_PROPERTY(Interval clipCurrentTime READ clipCurrentTime NOTIFY clipCurrentTimeChanged FINAL)
QML_ELEMENT
@@ -40,6 +41,7 @@ class MediaClip : public QObject, public QQmlParserStatus {
signals:
void clipStartChanged();
void clipEndChanged();
+ void clipDurationChanged();
void activeChanged(bool);
void clipCurrentTimeChanged();
void clipEnded();
@@ -59,6 +61,8 @@ class MediaClip : public QObject, public QQmlParserStatus {
qint64 clipEnd() const { return m_clipEnd; };
void setClipEnd(qint64 ms);
+ qint64 clipDuration() const { return m_clipEnd - m_clipStart; };
+
const Interval& clipCurrentTime() const { return m_currentFrameTime; };
void setActive(bool active);
diff --git a/src/MediaFX/media_manager.cpp b/src/MediaFX/media_manager.cpp
index b205f79..97184d7 100644
--- a/src/MediaFX/media_manager.cpp
+++ b/src/MediaFX/media_manager.cpp
@@ -17,14 +17,17 @@
#include "media_manager.h"
#include "media_clip.h"
-#include "session.h"
#include
+#include
+#include
+using namespace std::chrono;
-MediaManager::MediaManager(Session* session, QObject* parent)
+MediaManager::MediaManager(const microseconds& frameDuration, QQuickView* quickView, QObject* parent)
: QObject(parent)
- , m_session(session)
+ , m_frameDuration(frameDuration)
+ , m_quickView(quickView)
{
- connect(this, &MediaManager::finishEncoding, [this](bool immediate) { this->setEncodingState(immediate ? EncodingState::Stopped : EncodingState::Stopping); emit this->session()->exitApp(0); });
+ connect(this, &MediaManager::finishEncoding, [this]() { this->finishedEncoding = true; });
}
MediaManager* MediaManager::singletonInstance()
diff --git a/src/MediaFX/media_manager.h b/src/MediaFX/media_manager.h
index a6ee938..f6f2e88 100644
--- a/src/MediaFX/media_manager.h
+++ b/src/MediaFX/media_manager.h
@@ -17,47 +17,52 @@
#pragma once
+#include "interval.h"
#include
#include
#include
+#include
#include
#include
#include
+#include
class MediaClip;
-class Session;
+using namespace std::chrono;
class MediaManager : public QObject {
Q_OBJECT
+ Q_PROPERTY(QQuickView* window READ window FINAL)
public:
using QObject::QObject;
- MediaManager(Session* session, QObject* parent = nullptr);
+ MediaManager(const microseconds& frameDuration, QQuickView* quickView, QObject* parent = nullptr);
static MediaManager* singletonInstance();
- Session* session() { return m_session; };
+ Q_INVOKABLE Interval createInterval(qint64 start, qint64 end) const
+ {
+ return Interval(start, end);
+ };
+
+ QQuickView* window() const { return m_quickView; };
+ const microseconds& frameDuration() { return m_frameDuration; };
void registerClip(MediaClip* clip);
void unregisterClip(MediaClip* clip);
void render();
- enum EncodingState {
- Encoding,
- Stopping,
- Stopped,
- };
-
- EncodingState encodingState() const { return m_encodingState; };
- void setEncodingState(EncodingState state) { m_encodingState = state; };
+ bool isFinishedEncoding() const { return finishedEncoding; }
signals:
- void finishEncoding(bool immediate = true);
+ void finishEncoding();
+ void frameRendered();
private:
- Session* m_session;
+ microseconds m_frameDuration;
+ QQuickView* m_quickView;
QList activeClips;
- EncodingState m_encodingState = Encoding;
+ bool finishedEncoding = false;
};
// https://doc.qt.io/qt-6/qqmlengine.html#QML_SINGLETON
diff --git a/src/MediaFX/qml/MediaSequence.qml b/src/MediaFX/qml/MediaSequence.qml
new file mode 100644
index 0000000..c2b9ce1
--- /dev/null
+++ b/src/MediaFX/qml/MediaSequence.qml
@@ -0,0 +1,97 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+
+import QtQuick
+import QtMultimedia
+import MediaFX
+import "sequence.js" as Sequence
+
+Item {
+ id: root
+
+ default required property list mediaClips
+ required property list mediaMixers
+ property alias fillMode: video.fillMode
+ property alias orientation: video.orientation
+
+ signal mediaSequenceEnded
+
+ Item {
+ id: internal
+
+ property int currentClipIndex: 0
+ property int currentMixerIndex: 0
+ property int mixStartTime
+
+ anchors.fill: parent
+
+ states: [
+ State {
+ name: "video"
+
+ PropertyChanges {
+ Media.clip: root.mediaClips[internal.currentClipIndex]
+ target: video
+ }
+ PropertyChanges {
+ target: root.mediaMixers[internal.currentMixerIndex]
+ visible: false
+ }
+ },
+ State {
+ name: "mixer"
+
+ PropertyChanges {
+ Media.clip: root.mediaClips[internal.currentClipIndex]
+ layer.enabled: true
+ visible: false
+ target: video
+ }
+ PropertyChanges {
+ Media.clip: (internal.currentClipIndex + 1 >= root.mediaClips.length) ? null : root.mediaClips[internal.currentClipIndex + 1]
+ layer.enabled: true
+ target: auxVideo
+ }
+ ParentChange {
+ parent: root
+ target: root.mediaMixers[internal.currentMixerIndex]
+ }
+ PropertyChanges {
+ anchors.fill: root
+ source: video
+ dest: auxVideo
+ visible: true
+ target: root.mediaMixers[internal.currentMixerIndex]
+ }
+ }
+ ]
+
+ Component.onCompleted: Sequence.initializeClip()
+
+ VideoOutput {
+ id: video
+
+ anchors.fill: internal
+ }
+ VideoOutput {
+ id: auxVideo
+
+ fillMode: video.fillMode
+ orientation: video.orientation
+ anchors.fill: internal
+ visible: false
+ }
+ }
+}
diff --git a/src/MediaFX/qml/MultiEffectState.qml b/src/MediaFX/qml/MultiEffectState.qml
index 7cdba23..eec72e6 100644
--- a/src/MediaFX/qml/MultiEffectState.qml
+++ b/src/MediaFX/qml/MultiEffectState.qml
@@ -16,26 +16,30 @@
import QtQuick
import QtQuick.Effects
import QtMultimedia
-import MediaFX
State {
+ id: root
+
default required property MultiEffect effect
required property VideoOutput videoOutput
- Component.onCompleted: effect.visible = false
+ Component.onCompleted: root.effect.visible = false
ParentChange {
- target: effect
- parent: videoOutput.parent
+ parent: root.videoOutput.parent
+ target: root.effect
}
PropertyChanges {
- anchors.fill: videoOutput
- source: videoOutput
- target: effect
+ x: root.videoOutput.x
+ y: root.videoOutput.y
+ width: root.videoOutput.width
+ height: root.videoOutput.height
+ source: root.videoOutput
visible: true
+ target: root.effect
}
PropertyChanges {
- target: videoOutput
visible: false
+ target: root.videoOutput
}
}
diff --git a/src/MediaFX/qml/ShaderEffectState.qml b/src/MediaFX/qml/ShaderEffectState.qml
new file mode 100644
index 0000000..eacbb23
--- /dev/null
+++ b/src/MediaFX/qml/ShaderEffectState.qml
@@ -0,0 +1,30 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+
+import QtQuick
+import QtMultimedia
+
+State {
+ id: root
+
+ default required property Component effect
+ required property VideoOutput videoOutput
+
+ PropertyChanges {
+ layer.effect: root.effect
+ layer.enabled: true
+ target: root.videoOutput
+ }
+}
diff --git a/src/MediaFX/qml/effects/CrossFadeMixer.qml b/src/MediaFX/qml/effects/CrossFadeMixer.qml
new file mode 100644
index 0000000..7e34926
--- /dev/null
+++ b/src/MediaFX/qml/effects/CrossFadeMixer.qml
@@ -0,0 +1,20 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+
+import MediaFX
+
+MediaMixer {
+ fragmentShader: "qrc:/qml/effects/crossfade.frag.qsb"
+}
diff --git a/src/MediaFX/qml/effects/LumaGradientMixer.qml b/src/MediaFX/qml/effects/LumaGradientMixer.qml
new file mode 100644
index 0000000..5d71678
--- /dev/null
+++ b/src/MediaFX/qml/effects/LumaGradientMixer.qml
@@ -0,0 +1,39 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+
+import QtQuick
+import QtQuick.Shapes
+import MediaFX
+
+LumaMixer {
+ id: root
+
+ property alias fillGradient: path.fillGradient
+
+ Shape {
+ parent: root.parent
+ visible: false
+
+ ShapePath {
+ id: path
+
+ scale: Qt.size(root.width, root.height)
+
+ PathPolyline {
+ path: [Qt.point(0, 0), Qt.point(0, 1), Qt.point(1, 1), Qt.point(1, 0), Qt.point(0, 0)]
+ }
+ }
+ }
+}
diff --git a/src/MediaFX/qml/effects/LumaMixer.qml b/src/MediaFX/qml/effects/LumaMixer.qml
new file mode 100644
index 0000000..db9e0db
--- /dev/null
+++ b/src/MediaFX/qml/effects/LumaMixer.qml
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+
+import QtQuick
+import MediaFX
+
+MediaMixer {
+ id: root
+
+ default required property Item luma
+ property real transitionWidth: 1.0
+ readonly property real premultipliedTransitionWidth: root.time * (transitionWidth + 1.0)
+
+ fragmentShader: "qrc:/qml/effects/luma.frag.qsb"
+ state: "default"
+
+ states: State {
+ name: "default"
+
+ PropertyChanges {
+ x: root.x
+ y: root.y
+ width: root.width
+ height: root.height
+ layer.enabled: true
+ visible: false
+ target: root.luma
+ }
+ }
+}
diff --git a/src/MediaFX/qml/effects/MediaMixer.qml b/src/MediaFX/qml/effects/MediaMixer.qml
new file mode 100644
index 0000000..6b69720
--- /dev/null
+++ b/src/MediaFX/qml/effects/MediaMixer.qml
@@ -0,0 +1,25 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+
+import QtQuick
+
+ShaderEffect {
+ property Item source
+ property Item dest
+ property int duration: 1000
+ property real time: 0.0
+
+ visible: false
+}
diff --git a/src/MediaFX/qml/effects/WipeMixer.qml b/src/MediaFX/qml/effects/WipeMixer.qml
new file mode 100644
index 0000000..3681764
--- /dev/null
+++ b/src/MediaFX/qml/effects/WipeMixer.qml
@@ -0,0 +1,77 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+
+import QtQuick
+import QtQuick.Shapes
+import MediaFX
+
+LumaGradientMixer {
+ id: root
+
+ enum Direction {
+ Up,
+ Down,
+ Left,
+ Right
+ }
+
+ property int direction: WipeMixer.Direction.Right
+ property real blindsEffect: 0.0
+
+ function isDirectionHorizontal(): bool {
+ switch (root.direction) {
+ case WipeMixer.Direction.Up:
+ case WipeMixer.Direction.Down:
+ return false;
+ case WipeMixer.Direction.Left:
+ case WipeMixer.Direction.Right:
+ return true;
+ }
+ return false;
+ }
+
+ function isDirectionForward(): bool {
+ switch (root.direction) {
+ case WipeMixer.Direction.Up:
+ case WipeMixer.Direction.Left:
+ return false;
+ case WipeMixer.Direction.Down:
+ case WipeMixer.Direction.Right:
+ return true;
+ }
+ return false;
+ }
+
+ function calculatePosition(dimension: real): real {
+ return root.blindsEffect > 0 ? (dimension * root.blindsEffect) : dimension;
+ }
+
+ transitionWidth: 1.0
+
+ fillGradient: LinearGradient {
+ spread: root.blindsEffect > 0 ? ShapeGradient.RepeatSpread : ShapeGradient.PadSpread
+ x2: isDirectionHorizontal() ? calculatePosition(root.width) : 0
+ y2: isDirectionHorizontal() ? 0 : calculatePosition(root.height)
+
+ GradientStop {
+ color: "white"
+ position: isDirectionForward() ? 0.0 : 1
+ }
+ GradientStop {
+ color: "black"
+ position: isDirectionForward() ? 1.0 : 0.0
+ }
+ }
+}
diff --git a/src/MediaFX/qml/effects/crossfade.frag b/src/MediaFX/qml/effects/crossfade.frag
new file mode 100644
index 0000000..dbcb9d3
--- /dev/null
+++ b/src/MediaFX/qml/effects/crossfade.frag
@@ -0,0 +1,29 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+#version 440
+layout(location = 0) in vec2 qt_TexCoord0;
+layout(location = 0) out vec4 fragColor;
+layout(std140, binding = 0) uniform buf {
+ mat4 qt_Matrix;
+ float qt_Opacity;
+ float time;
+};
+layout(binding = 1) uniform sampler2D source;
+layout(binding = 2) uniform sampler2D dest;
+void main() {
+ vec4 sp = texture(source, qt_TexCoord0);
+ vec4 dp = texture(dest, qt_TexCoord0);
+ fragColor = mix(sp, dp, time) * qt_Opacity;
+}
\ No newline at end of file
diff --git a/src/MediaFX/qml/effects/luma.frag b/src/MediaFX/qml/effects/luma.frag
new file mode 100644
index 0000000..f4d8ca8
--- /dev/null
+++ b/src/MediaFX/qml/effects/luma.frag
@@ -0,0 +1,36 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+#version 440
+layout(location = 0) in vec2 qt_TexCoord0;
+layout(location = 0) out vec4 fragColor;
+layout(std140, binding = 0) uniform buf {
+ mat4 qt_Matrix;
+ float qt_Opacity;
+ float transitionWidth;
+ float premultipliedTransitionWidth;
+};
+layout(binding = 1) uniform sampler2D source;
+layout(binding = 2) uniform sampler2D dest;
+layout(binding = 3) uniform sampler2D luma;
+void main() {
+ vec4 sp = texture(source, qt_TexCoord0);
+ vec4 dp = texture(dest, qt_TexCoord0);
+ vec4 lp = texture(luma, qt_TexCoord0);
+
+ // Based on https://github.com/j-b-m/movit/blob/master/luma_mix_effect.frag
+ // premultipliedTransitionWidth = time * (transitionWidth + 1.0)
+ float m = clamp((lp.r * transitionWidth - transitionWidth) + premultipliedTransitionWidth, 0.0, 1.0);
+ fragColor = mix(sp, dp, m) * qt_Opacity;
+}
\ No newline at end of file
diff --git a/src/MediaFX/qml/sequence.js b/src/MediaFX/qml/sequence.js
new file mode 100644
index 0000000..2b88dd0
--- /dev/null
+++ b/src/MediaFX/qml/sequence.js
@@ -0,0 +1,55 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+
+function onClipEnded() {
+ MediaManager.frameRendered.connect(nextClip);
+};
+
+function onClipCurrentTimeChanged() {
+ var clip = root.mediaClips[internal.currentClipIndex];
+ if (clip.clipCurrentTime.start >= internal.mixStartTime) {
+ internal.state = "mixer";
+ root.mediaMixers[internal.currentMixerIndex].time = (clip.clipCurrentTime.start - internal.mixStartTime) / (clip.clipEnd - internal.mixStartTime);
+ }
+};
+
+function nextClip() {
+ MediaManager.frameRendered.disconnect(nextClip);
+ if (internal.currentClipIndex + 1 < root.mediaClips.length) {
+ var clip = root.mediaClips[internal.currentClipIndex];
+ clip.clipCurrentTimeChanged.disconnect(onClipCurrentTimeChanged);
+ clip.clipEnded.disconnect(onClipEnded);
+ internal.currentClipIndex += 1;
+ initializeClip();
+ }
+ internal.currentMixerIndex = (internal.currentMixerIndex + 1) % root.mediaMixers.length;
+ root.mediaMixers[internal.currentMixerIndex].time = 0;
+};
+
+function initializeClip() {
+ var clip = root.mediaClips[internal.currentClipIndex];
+ // Last clip
+ if (internal.currentClipIndex >= root.mediaClips.length - 1) {
+ clip.onClipEnded.connect(root.mediaSequenceEnded)
+ }
+ else {
+ var mixer = root.mediaMixers[internal.currentMixerIndex];
+ var clampedMixDuration = Math.min(Math.min(mixer.duration, clip.clipDuration), root.mediaClips[internal.currentClipIndex + 1].clipDuration);
+ internal.mixStartTime = clip.clipEnd - clampedMixDuration;
+ clip.clipCurrentTimeChanged.connect(onClipCurrentTimeChanged);
+ clip.clipEnded.connect(onClipEnded);
+ }
+ internal.state = "video";
+};
diff --git a/src/MediaFX/session.cpp b/src/MediaFX/session.cpp
index eab7dfa..42c0761 100644
--- a/src/MediaFX/session.cpp
+++ b/src/MediaFX/session.cpp
@@ -28,6 +28,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -50,11 +51,13 @@ Session::Session(Encoder* encoder, QObject* parent)
, quickView(nullptr)
{
FFMS_Init(0, 0);
- connect(this, &Session::exitApp, qApp, &QCoreApplication::exit, Qt::QueuedConnection);
animationDriver->install();
renderControl.reset(new RenderControl());
quickView.reset(new QQuickView(QUrl(), renderControl.get()));
+ // Enables Qt.exit(0) in QML
+ connect(quickView->engine(), &QQmlEngine::exit, qApp, &QCoreApplication::exit, Qt::QueuedConnection);
+
#ifdef MEDIAFX_ENABLE_VULKAN
if (quickView->rendererInterface()->graphicsApi() == QSGRendererInterface::Vulkan) {
vulkanInstance.setExtensions(QQuickGraphicsConfiguration::preferredInstanceExtensions());
@@ -62,7 +65,7 @@ Session::Session(Encoder* encoder, QObject* parent)
}
#endif
- manager = new MediaManager(this, this);
+ manager = new MediaManager(m_frameDuration, quickView.get(), this);
MediaManagerForeign::s_singletonInstance = manager;
quickView->setResizeMode(QQuickView::ResizeMode::SizeRootObjectToView);
@@ -102,8 +105,9 @@ bool Session::initialize(const QUrl& url)
void Session::quickViewStatusChanged(QQuickView::Status status)
{
if (status == QQuickView::Error) {
- emit exitApp(1);
+ emit quickView->engine()->exit(1);
} else if (status == QQuickView::Ready) {
+ quickView->rootObject()->setEnabled(false);
QCoreApplication::postEvent(this, new QEvent(renderEventType));
}
}
@@ -142,21 +146,18 @@ void Session::render()
{
manager->render();
- switch (manager->encodingState()) {
- case MediaManager::EncodingState::Encoding:
- break;
- case MediaManager::EncodingState::Stopped:
- return;
- case MediaManager::EncodingState::Stopping:
- manager->setEncodingState(MediaManager::EncodingState::Stopped);
- break;
- }
-
auto frameData = renderControl->renderVideoFrame();
if (write(encoder->videofd(), frameData.size(), frameData.constData()) == -1)
return;
+ emit manager->frameRendered();
+
+ if (manager->isFinishedEncoding()) {
+ emit quickView->engine()->exit(0);
+ return;
+ }
+
animationDriver->advance();
QCoreApplication::postEvent(this, new QEvent(renderEventType));
diff --git a/src/MediaFX/session.h b/src/MediaFX/session.h
index f958ef3..7a3c97a 100644
--- a/src/MediaFX/session.h
+++ b/src/MediaFX/session.h
@@ -53,9 +53,6 @@ class Session : public QObject {
bool event(QEvent* event) override;
-signals:
- void exitApp(int);
-
private slots:
void quickViewStatusChanged(QQuickView::Status status);
void engineWarnings(const QList& warnings);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 9d9510e..2011795 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -35,13 +35,14 @@ macro(add_compile_shaders)
endmacro()
macro(add_qml_test)
- cmake_parse_arguments(QML_TEST "" "NAME;OUTPUTSPEC;QMLFILE;OUTPUTFILE" "ASSETSPECS" ${ARGN})
+ cmake_parse_arguments(QML_TEST "" "NAME;OUTPUTSPEC;QMLFILE;OUTPUTFILE;THRESHOLD" "ASSETSPECS" ${ARGN})
add_test(NAME ${QML_TEST_NAME} COMMAND
${CMAKE_CURRENT_SOURCE_DIR}/qmltest.sh
$
${QML_TEST_OUTPUTSPEC}
${CMAKE_CURRENT_SOURCE_DIR}/qml/${QML_TEST_QMLFILE}
${CMAKE_CURRENT_SOURCE_DIR}/../build/${CMAKE_SYSTEM_NAME}/output/${QML_TEST_OUTPUTFILE}
+ ${QML_TEST_THRESHOLD}
${QML_TEST_ASSETSPECS}
)
set_tests_properties(${QML_TEST_NAME} PROPERTIES DEPENDS "tst_shaders")
@@ -62,13 +63,14 @@ qt_add_executable(tst_encoder tst_encoder.cpp)
add_test(NAME tst_encoder COMMAND tst_encoder)
target_link_libraries(tst_encoder PRIVATE mediafx Qt::Test)
-add_qml_test(NAME tst_qml_static OUTPUTSPEC 15:320x180 QMLFILE static.qml OUTPUTFILE static.nut)
-add_qml_test(NAME tst_qml_animated OUTPUTSPEC 15:320x180 QMLFILE animated.qml OUTPUTFILE animated.nut)
-add_qml_test(NAME tst_qml_video_clipstart OUTPUTSPEC 15:320x180 QMLFILE video-clipstart.qml OUTPUTFILE video-clipstart.nut ASSETSPECS nut:red:320x180:15:8)
-add_qml_test(NAME tst_qml_multisink OUTPUTSPEC 30:640x360 QMLFILE multisink.qml OUTPUTFILE multisink.nut ASSETSPECS nut:red:640x360:30:4 png:red:160x120)
-add_qml_test(NAME tst_qml_video_ad_insertion OUTPUTSPEC 30:320x180 QMLFILE video-ad-insertion.qml OUTPUTFILE video-ad-insertion.nut ASSETSPECS nut:red:320x180:15:8 nut:blue:320x180:30:3)
-add_qml_test(NAME tst_qml_video_multieffect OUTPUTSPEC 30:320x180 QMLFILE video-multieffect.qml OUTPUTFILE video-multieffect.nut ASSETSPECS nut:blue:320x180:30:3)
-add_qml_test(NAME tst_qml_video_shadereffect OUTPUTSPEC 30:320x180 QMLFILE video-shadereffect.qml OUTPUTFILE video-shadereffect.nut ASSETSPECS nut:blue:320x180:30:3)
+add_qml_test(NAME tst_qml_static OUTPUTSPEC 15:320x180 QMLFILE static.qml OUTPUTFILE static.nut THRESHOLD 99.999)
+add_qml_test(NAME tst_qml_animated OUTPUTSPEC 15:320x180 QMLFILE animated.qml OUTPUTFILE animated.nut THRESHOLD 99.999)
+add_qml_test(NAME tst_qml_video_clipstart OUTPUTSPEC 15:320x180 QMLFILE video-clipstart.qml OUTPUTFILE video-clipstart.nut THRESHOLD 99.999 ASSETSPECS nut:red:320x180:15:8)
+add_qml_test(NAME tst_qml_multisink OUTPUTSPEC 30:640x360 QMLFILE multisink.qml OUTPUTFILE multisink.nut THRESHOLD 99.999 ASSETSPECS nut:red:640x360:30:4 png:red:160x120)
+add_qml_test(NAME tst_qml_video_ad_insertion OUTPUTSPEC 30:320x180 QMLFILE video-ad-insertion.qml OUTPUTFILE video-ad-insertion.nut THRESHOLD 99.999 ASSETSPECS nut:red:320x180:15:8 nut:blue:320x180:30:3)
+add_qml_test(NAME tst_qml_video_multieffect OUTPUTSPEC 30:320x180 QMLFILE video-multieffect.qml OUTPUTFILE video-multieffect.nut THRESHOLD 99.999 ASSETSPECS nut:blue:320x180:30:3)
+add_qml_test(NAME tst_qml_video_shadereffect OUTPUTSPEC 30:320x180 QMLFILE video-shadereffect.qml OUTPUTFILE video-shadereffect.nut THRESHOLD 99.999 ASSETSPECS nut:blue:320x180:30:3)
+add_qml_test(NAME tst_qml_sequence OUTPUTSPEC 15:320x180 QMLFILE sequence.qml OUTPUTFILE sequence.nut THRESHOLD 98.999 ASSETSPECS nut:red:320x180:15:8 nut:green:320x180:15:3 nut:yellow:320x180:15:3 png:red:160x120)
# Label tests that require a GPU
-set_tests_properties(tst_qml_static tst_qml_animated tst_qml_video_clipstart tst_qml_multisink tst_qml_video_ad_insertion tst_qml_video_multieffect tst_qml_video_shadereffect PROPERTIES LABELS GPU)
\ No newline at end of file
+set_tests_properties(tst_qml_static tst_qml_animated tst_qml_video_clipstart tst_qml_multisink tst_qml_video_ad_insertion tst_qml_video_multieffect tst_qml_video_shadereffect tst_qml_sequence PROPERTIES LABELS GPU)
\ No newline at end of file
diff --git a/tests/fixtures b/tests/fixtures
index 0cb9c12..93fe4bd 160000
--- a/tests/fixtures
+++ b/tests/fixtures
@@ -1 +1 @@
-Subproject commit 0cb9c12bdd4420af151eb30aca4cb2526a55efc5
+Subproject commit 93fe4bdedfb07f999849d0fed5d7eb2b1d49ced9
diff --git a/tests/qml/animated.qml b/tests/qml/animated.qml
index 920eaa4..0f3e701 100644
--- a/tests/qml/animated.qml
+++ b/tests/qml/animated.qml
@@ -31,7 +31,7 @@ Item {
transitions: Transition {
onRunningChanged: {
if (!running)
- MediaManager.finishEncoding(false);
+ MediaManager.finishEncoding();
}
AnchorAnimation {
@@ -44,8 +44,8 @@ Item {
Rectangle {
id: rect
- color: "red"
- height: 50
width: 50
+ height: 50
+ color: "red"
}
}
diff --git a/tests/qml/sequence.qml b/tests/qml/sequence.qml
new file mode 100644
index 0000000..6125554
--- /dev/null
+++ b/tests/qml/sequence.qml
@@ -0,0 +1,62 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+
+import QtQuick
+import QtQuick.Effects
+import QtMultimedia
+import MediaFX
+
+Item {
+ MediaSequence {
+ id: sequence
+
+ anchors.fill: parent
+
+ mediaMixers: [
+ CrossFadeMixer {
+ },
+ WipeMixer {
+ direction: WipeMixer.Direction.Down
+ blindsEffect: 0.05
+ },
+ WipeMixer {
+ direction: WipeMixer.Direction.Right
+ transitionWidth: 2.0
+ }
+ ]
+
+ Component.onCompleted: {
+ sequence.mediaSequenceEnded.connect(MediaManager.finishEncoding);
+ }
+
+ MediaClip {
+ source: Qt.resolvedUrl("../fixtures/assets/blue-320x180-30fps-3s.nut")
+ }
+ MediaClip {
+ clipEnd: 3000
+ source: Qt.resolvedUrl("../fixtures/assets/red-320x180-15fps-8s.nut")
+ }
+ MediaClip {
+ source: Qt.resolvedUrl("../fixtures/assets/green-320x180-15fps-3s.nut")
+ }
+ MediaClip {
+ clipEnd: 3000
+ source: Qt.resolvedUrl("../fixtures/assets/red-160x120.png")
+ }
+ MediaClip {
+ source: Qt.resolvedUrl("../fixtures/assets/yellow-320x180-15fps-3s.nut")
+ }
+ }
+}
diff --git a/tests/qml/static.qml b/tests/qml/static.qml
index 3b913a6..16832f2 100644
--- a/tests/qml/static.qml
+++ b/tests/qml/static.qml
@@ -19,5 +19,5 @@ import MediaFX
Rectangle {
color: "red"
- Component.onCompleted: MediaManager.finishEncoding(false)
+ Component.onCompleted: MediaManager.finishEncoding()
}
diff --git a/tests/qml/video-multieffect.qml b/tests/qml/video-multieffect.qml
index e7d0dcf..10bff3b 100644
--- a/tests/qml/video-multieffect.qml
+++ b/tests/qml/video-multieffect.qml
@@ -31,8 +31,8 @@ Item {
VideoOutput {
id: videoOutput
- Media.clip: videoClip
anchors.fill: parent
+ Media.clip: videoClip
states: MultiEffectState {
name: "filter"
diff --git a/tests/qml/video-shadereffect.qml b/tests/qml/video-shadereffect.qml
index 6335a61..dcacdc7 100644
--- a/tests/qml/video-shadereffect.qml
+++ b/tests/qml/video-shadereffect.qml
@@ -30,22 +30,17 @@ Item {
VideoOutput {
id: videoOutput
- Media.clip: videoClip
anchors.fill: parent
- layer.enabled: false
- // layer.samplerName: "source"
+ Media.clip: videoClip
- layer.effect: ShaderEffect {
- fragmentShader: "grayscale.frag.qsb"
- }
- states: State {
+ states: ShaderEffectState {
name: "filter"
+ videoOutput: videoOutput
// From 1-2 sec into the video, switch to greyscale
when: (videoClip.clipCurrentTime.containedBy(1000, 2000))
- PropertyChanges {
- layer.enabled: true
- target: videoOutput
+ ShaderEffect {
+ fragmentShader: "grayscale.frag.qsb"
}
}
}
diff --git a/tests/qmltest.sh b/tests/qmltest.sh
index 681205e..7f17889 100755
--- a/tests/qmltest.sh
+++ b/tests/qmltest.sh
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License along with mediaFX.
# If not, see .
-usage="$0 : [asset-spec ...]"
+usage="$0 : [asset-spec ...]"
BASE=${BASH_SOURCE%/*}
@@ -29,6 +29,8 @@ QML=${1:?$usage}
shift
OUTPUT=${1:?$usage}
shift
+THRESHOLD=${1:?$usage}
+shift
mkdir -p $(dirname "${OUTPUT}")
for assetspec in "$@"; do
@@ -48,5 +50,5 @@ FIXTURE=${FIXTURES}/$(basename "${OUTPUT}")
if ( ! diff "${FIXTURE}.framehash" "${OUTPUT}.framehash" ); then
echo Warning: framehash is different, comparing frames
mkdir -p "${OUTPUT}-png"
- ( ! ffmpeg -hide_banner -i "${FIXTURE}" -i "${OUTPUT}" -filter_complex "blend=all_mode=difference,blackframe=amount=0:threshold=3,metadata=select:key=lavfi.blackframe.pblack:value=99.999:function=less,metadata=print:file=-" -an -v 24 "${OUTPUT}-png/frame-%05d.png" | grep pblack ) || exit 1
+ ( ! ffmpeg -hide_banner -i "${FIXTURE}" -i "${OUTPUT}" -filter_complex "blend=all_mode=difference,blackframe=amount=0:threshold=3,metadata=select:key=lavfi.blackframe.pblack:value=${THRESHOLD}:function=less,metadata=print:file=-" -an -v 24 "${OUTPUT}-png/frame-%05d.png" | grep pblack ) || exit 1
fi
diff --git a/tools/qml/LumaMixerViewer.qml b/tools/qml/LumaMixerViewer.qml
new file mode 100644
index 0000000..9f07cb1
--- /dev/null
+++ b/tools/qml/LumaMixerViewer.qml
@@ -0,0 +1,179 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Shapes
+import MediaFX
+import MediaFX.Viewer
+
+ColumnLayout {
+ id: layout
+
+ Component.onCompleted: MediaManager.window.color = palette.window
+
+ SystemPalette {
+ id: palette
+
+ colorGroup: SystemPalette.Active
+ }
+ MediaMixerViewer {
+ mixer: linearMixer
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ LabeledSlider {
+ id: x1Slider
+
+ label: "x1"
+ to: linearMixer.width
+ Layout.fillWidth: true
+ }
+ LabeledSlider {
+ id: y1Slider
+
+ label: "y1"
+ to: linearMixer.height
+ Layout.fillWidth: true
+ }
+ LabeledSlider {
+ id: x2Slider
+
+ label: "x2"
+ to: linearMixer.width
+ Layout.fillWidth: true
+ }
+ LabeledSlider {
+ id: y2Slider
+
+ label: "y2"
+ to: linearMixer.height
+ Layout.fillWidth: true
+ }
+ LabeledSlider {
+ id: tw1Slider
+
+ label: "transitionWidth"
+ to: linearMixer.width
+ Layout.fillWidth: true
+ }
+ LumaGradientMixer {
+ id: linearMixer
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ transitionWidth: tw1Slider.value
+ fillGradient: LinearGradient {
+ x1: x1Slider.value
+ y1: y1Slider.value
+ x2: x2Slider.value
+ y2: y2Slider.value
+
+ GradientStop {
+ position: 0.0
+ color: "white"
+ }
+ GradientStop {
+ position: 1.0
+ color: "black"
+ }
+ }
+ }
+ }
+ }
+ MediaMixerViewer {
+ mixer: conicalMixer
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ LabeledSlider {
+ id: angleSlider
+
+ label: "angle"
+ to: 360
+ Layout.fillWidth: true
+ }
+ LabeledSlider {
+ id: centerXSlider
+
+ label: "centerX"
+ to: conicalMixer.width
+ Layout.fillWidth: true
+ }
+ LabeledSlider {
+ id: centerYSlider
+
+ label: "centerY"
+ to: conicalMixer.height
+ Layout.fillWidth: true
+ }
+ LabeledSlider {
+ id: tw2Slider
+
+ label: "transitionWidth"
+ to: conicalMixer.width
+ Layout.fillWidth: true
+ }
+ LumaGradientMixer {
+ id: conicalMixer
+
+ transitionWidth: tw2Slider.value
+
+ fillGradient: ConicalGradient {
+ angle: angleSlider.value
+ centerX: centerXSlider.value
+ centerY: centerYSlider.value
+
+ GradientStop {
+ position: 0.0
+ color: "white"
+ }
+ GradientStop {
+ position: 1.0
+ color: "black"
+ }
+ }
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ }
+ }
+
+ component LabeledSlider: RowLayout {
+ property alias label: label.text
+ property alias from: slider.from
+ property alias to: slider.to
+ property alias value: slider.value
+
+ Label {
+ id: label
+ }
+ Slider {
+ id: slider
+
+ Layout.fillWidth: true
+ ToolTip.delay: 1000
+ ToolTip.text: slider.value
+ ToolTip.visible: hovered
+ hoverEnabled: true
+ }
+ }
+}
diff --git a/tools/viewer/CMakeLists.txt b/tools/viewer/CMakeLists.txt
new file mode 100644
index 0000000..cbac996
--- /dev/null
+++ b/tools/viewer/CMakeLists.txt
@@ -0,0 +1,28 @@
+# Copyright (C) 2024 Andrew Wason
+#
+# This file is part of mediaFX.
+#
+# mediaFX is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software Foundation,
+# either version 3 of the License, or (at your option) any later version.
+#
+# mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+# See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with mediaFX.
+# If not, see .
+
+qt_add_executable(mediafxviewer
+ main.cpp
+)
+
+target_link_libraries(mediafxviewer PUBLIC mediafx mediafxplugin)
+
+qt_add_qml_module(mediafxviewer
+ URI MediaFX.Viewer
+ QML_FILES
+ qml/MediaMixerViewer.qml
+)
+
+install(TARGETS mediafxviewer RUNTIME DESTINATION)
diff --git a/tools/viewer/main.cpp b/tools/viewer/main.cpp
new file mode 100644
index 0000000..fea2868
--- /dev/null
+++ b/tools/viewer/main.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 Andrew Wason
+ *
+ * This file is part of mediaFX.
+ *
+ * mediaFX is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with mediaFX.
+ * If not, see .
+ */
+
+#include "application.h"
+#include "media_manager.h"
+#include
+#include
+#include
+#include
+#include
+#include
+using namespace std::chrono;
+
+int main(int argc, char* argv[])
+{
+ initializeMediaFX();
+ QGuiApplication app(argc, argv);
+
+ app.setOrganizationDomain(qSL("mediafx.stream"));
+ app.setOrganizationName(qSL("mediaFX"));
+ app.setApplicationName(qSL("mediaFX viewer"));
+
+ QCommandLineParser parser;
+ parser.setApplicationDescription(qSL("mediaFX viewer"));
+ parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
+ parser.addHelpOption();
+ parser.addPositionalArgument(qSL("source"), qSL("QML source URL."));
+
+ parser.process(app);
+
+ const QStringList args = parser.positionalArguments();
+ if (args.size() != 1) {
+ qCritical("Missing required source url");
+ parser.showHelp(1);
+ }
+ QUrl url(args.at(0));
+
+ QQuickView quickView;
+ MediaManager* manager = new MediaManager(microseconds(33333), &quickView, &app);
+ MediaManagerForeign::s_singletonInstance = manager;
+ quickView.setSource(url);
+ quickView.setResizeMode(QQuickView::ResizeMode::SizeRootObjectToView);
+ quickView.show();
+
+ return app.exec();
+}
diff --git a/tools/viewer/qml/MediaMixerViewer.qml b/tools/viewer/qml/MediaMixerViewer.qml
new file mode 100644
index 0000000..096e8ac
--- /dev/null
+++ b/tools/viewer/qml/MediaMixerViewer.qml
@@ -0,0 +1,114 @@
+// Copyright (C) 2024 Andrew Wason
+//
+// This file is part of mediaFX.
+//
+// mediaFX is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// mediaFX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with mediaFX.
+// If not, see .
+import QtQuick
+import QtQuick.Shapes
+import QtQuick.Layouts
+import QtQuick.Controls
+import MediaFX
+
+Item {
+ id: root
+
+ default property alias data: mixerContainer.data
+ required property Item mixer
+
+ implicitHeight: 400
+ implicitWidth: 400
+ state: "default"
+
+ states: State {
+ name: "default"
+
+ PropertyChanges {
+ dest: destItem
+ source: sourceItem
+ time: time.value
+ visible: true
+ target: root.mixer
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ Item {
+ id: mixerContainer
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+ Slider {
+ id: time
+
+ Layout.fillWidth: true
+ from: 0
+ to: 1
+ value: 0.5
+ }
+ }
+ Rectangle {
+ id: sourceItem
+
+ anchors.fill: parent
+ color: "lightblue"
+ layer.enabled: true
+ visible: false
+
+ Shape {
+ anchors.fill: parent
+
+ ShapePath {
+ fillColor: "orange"
+ fillRule: ShapePath.WindingFill
+ scale: Qt.size(sourceItem.width / 160, sourceItem.height / 160)
+ strokeColor: "darkgray"
+ strokeWidth: 3
+
+ PathPolyline {
+ path: [Qt.point(80, 0), Qt.point(130, 160), Qt.point(0, 55), Qt.point(160, 55), Qt.point(30, 160), Qt.point(80, 0),]
+ }
+ }
+ }
+ }
+ Rectangle {
+ id: destItem
+
+ anchors.fill: parent
+ color: "yellow"
+ layer.enabled: true
+ visible: false
+
+ Shape {
+ id: qt
+
+ anchors.fill: parent
+
+ ShapePath {
+ fillColor: "green"
+ scale: Qt.size(destItem.width / 200, destItem.height / 200)
+ strokeColor: "darkGray"
+ strokeWidth: 3
+
+ PathAngleArc {
+ centerX: 100
+ centerY: 100
+ radiusX: 100
+ radiusY: 100
+ sweepAngle: 360
+ }
+ }
+ }
+ }
+}