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 + } + } + } + } +}