From f597f1c5b4061540feb260b4af42d3f8d23feae6 Mon Sep 17 00:00:00 2001 From: Andrew Wason Date: Fri, 10 May 2024 17:31:27 -0400 Subject: [PATCH] Support transforming video (pan, zoom, rotate) --- src/MediaFX/CMakeLists.txt | 1 + src/MediaFX/MediaSequence.qml | 40 ++++++++++++++++----- src/MediaFX/MediaSequenceClip.qml | 2 ++ src/MediaFX/Transformer.qml | 42 ++++++++++++++++++++++ src/MediaFX/render_session.cpp | 4 +++ src/MediaFX/sequence.js | 27 +++++++------- tests/CMakeLists.txt | 1 + tests/fixtures | 2 +- tests/qml/transformer.qml | 59 +++++++++++++++++++++++++++++++ 9 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 src/MediaFX/Transformer.qml create mode 100644 tests/qml/transformer.qml diff --git a/src/MediaFX/CMakeLists.txt b/src/MediaFX/CMakeLists.txt index e16ed28..c117eee 100644 --- a/src/MediaFX/CMakeLists.txt +++ b/src/MediaFX/CMakeLists.txt @@ -69,6 +69,7 @@ qt_add_qml_module(mediafx MediaSequenceClip.qml MultiEffectState.qml ShaderEffectState.qml + Transformer.qml DEPENDENCIES QtQuick QtQuick.Controls diff --git a/src/MediaFX/MediaSequence.qml b/src/MediaFX/MediaSequence.qml index d9cb3dd..e30cff8 100644 --- a/src/MediaFX/MediaSequence.qml +++ b/src/MediaFX/MediaSequence.qml @@ -56,16 +56,40 @@ Item { visible: false anchors.fill: internal } - VideoRenderer { - id: _mainVideoRenderer - anchors.fill: internal + VideoContainer { + id: _mainVideoContainer + VideoRenderer { + id: _mainVideoRenderer + anchors.fill: parent + mediaClip: _mainVideoContainer.mediaClip + transform: _mainVideoContainer.mediaClip?.transformer?.transform || null + } } - VideoRenderer { - id: _auxVideoRenderer - fillMode: _mainVideoRenderer.fillMode - orientation: _mainVideoRenderer.orientation + VideoContainer { + id: _auxVideoContainer visible: false - anchors.fill: internal + VideoRenderer { + id: _auxVideoRenderer + fillMode: _mainVideoRenderer.fillMode + orientation: _mainVideoRenderer.orientation + anchors.fill: parent + mediaClip: _auxVideoContainer.mediaClip + transform: _auxVideoContainer.mediaClip?.transformer?.transform || null + } + } + } + + component VideoContainer: Item { + property MediaSequenceClip mediaClip + + clip: true + anchors.fill: internal + + onMediaClipChanged: { + if (mediaClip && mediaClip.transformer) { + mediaClip.transformer.width = Qt.binding(() => width); + mediaClip.transformer.height = Qt.binding(() => height); + } } } } diff --git a/src/MediaFX/MediaSequenceClip.qml b/src/MediaFX/MediaSequenceClip.qml index 2884823..7ed3207 100644 --- a/src/MediaFX/MediaSequenceClip.qml +++ b/src/MediaFX/MediaSequenceClip.qml @@ -13,4 +13,6 @@ import MediaFX.Transition MediaClip { /*! The \l MediaTransition to use at the end of this clip to transition to the next clip. */ property MediaTransition endTransition + /*! A \l Transformer to transform the video */ + property Transformer transformer } diff --git a/src/MediaFX/Transformer.qml b/src/MediaFX/Transformer.qml new file mode 100644 index 0000000..c55d8ed --- /dev/null +++ b/src/MediaFX/Transformer.qml @@ -0,0 +1,42 @@ +// Copyright (C) 2024 Andrew Wason +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick + +/*! + \qmltype Transformer + \inqmlmodule MediaFX + \inherits QtObject + \brief Computes pan, zoom, rotate transformation +*/ +QtObject { + /*! Scale factor. */ + property real scale: 1 + /*! Rotation in degrees. */ + property real rotate: 0 + /*! Translation, normalized to -1..0..1. */ + property point translate: Qt.point(0, 0) + /*! Width of item being transformed. */ + property real width + /*! Height of item being transformed. */ + property real height + /*! The transformation matrix. */ + readonly property Matrix4x4 transform: Matrix4x4 { + matrix: computeTransform(width, height, scale, rotate, translate) + } + + function computeTransform(width: real, height: real, scale: real, rotate: real, translate: point): matrix4x4 { + const matrix = Qt.matrix4x4(); + if (scale === 1 && rotate === 0 && translate.x === 0 && translate.y === 0) + return matrix; + matrix.translate(Qt.vector3d(width / 2, height / 2, 0)); + if (translate.x !== 0 || translate.y !== 0) + matrix.translate(Qt.vector3d(-translate.x * width, -translate.y * height, 0)); + if (scale !== 1) + matrix.scale(scale); + if (rotate !== 0) + matrix.rotate(rotate, Qt.vector3d(0, 0, 1)); + matrix.translate(Qt.vector3d(-width / 2, -height / 2, 0)); + return matrix; + } +} diff --git a/src/MediaFX/render_session.cpp b/src/MediaFX/render_session.cpp index db57ca5..992ffb5 100644 --- a/src/MediaFX/render_session.cpp +++ b/src/MediaFX/render_session.cpp @@ -122,6 +122,10 @@ void RenderSession::componentComplete() m_loadedItem = qobject_cast(object); if (!m_loadedItem) { qmlWarning(this) << "Failed to load" << m_sourceUrl; + if (component.isError()) { + for (auto& error : component.errors()) + qCritical() << error; + } fatalError(); return; } diff --git a/src/MediaFX/sequence.js b/src/MediaFX/sequence.js index d9c1792..11a5e06 100644 --- a/src/MediaFX/sequence.js +++ b/src/MediaFX/sequence.js @@ -10,15 +10,19 @@ function onCurrentFrameTimechanged() { if (internal.transitionStartTime > 0 && clip.currentFrameTime.start >= internal.transitionStartTime) { if (clip.endTransition) { if (!clip.endTransition.parent) { + if (!internal.nextClip && internal.currentClipIndex < root.mediaClips.length - 1) { + internal.nextClip = root.mediaClips[internal.currentClipIndex + 1].createObject(null); + } + clip.endTransition.parent = _transitionContainer; clip.endTransition.anchors.fill = _transitionContainer; - clip.endTransition.source = _mainVideoRenderer; - clip.endTransition.dest = _auxVideoRenderer; + clip.endTransition.source = _mainVideoContainer; + clip.endTransition.dest = _auxVideoContainer; root.currentTransition = clip.endTransition; - _mainVideoRenderer.mediaClip = internal.currentClip; - _auxVideoRenderer.mediaClip = internal.nextClip; - _mainVideoRenderer.visible = false; + _mainVideoContainer.mediaClip = internal.currentClip; + _auxVideoContainer.mediaClip = internal.nextClip; + _mainVideoContainer.visible = false; _transitionContainer.visible = true; } clip.endTransition.time = (clip.currentFrameTime.start - internal.transitionStartTime) / (clip.endTime - internal.transitionStartTime); @@ -56,12 +60,9 @@ function initializeClip() { internal.currentClip.clipEnded.connect(root.mediaSequenceEnded) } else { - if (!internal.nextClip) { - internal.nextClip = root.mediaClips[internal.currentClipIndex + 1].createObject(null); - } const transition = internal.currentClip.endTransition; - if (transition && internal.nextClip) { - const clampedTransitionDuration = Math.min(Math.min(transition.duration, internal.currentClip.duration), internal.nextClip.duration); + if (transition) { + const clampedTransitionDuration = Math.min(transition.duration, internal.currentClip.duration); internal.transitionStartTime = internal.currentClip.endTime - clampedTransitionDuration; } else { @@ -71,8 +72,8 @@ function initializeClip() { internal.currentClip.clipEnded.connect(onClipEnded); } - _mainVideoRenderer.mediaClip = internal.currentClip; - _auxVideoRenderer.mediaClip = null; - _mainVideoRenderer.visible = true; + _mainVideoContainer.mediaClip = internal.currentClip; + _auxVideoContainer.mediaClip = null; + _mainVideoContainer.visible = true; _transitionContainer.visible = false; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5e46584..14614ef 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -65,6 +65,7 @@ add_qml_test(NAME tst_qml_sequence OUTPUTSPEC 15:320x180 QMLFILE sequence.qml OU add_qml_test(NAME tst_qml_demo OUTPUTSPEC 15:320x180 QMLFILE demo.qml OUTPUTFILE demo.nut THRESHOLD 99.999) add_qml_test(NAME tst_qml_async OUTPUTSPEC 15:320x180 QMLFILE async.qml OUTPUTFILE async.nut THRESHOLD 99.999) add_qml_test(NAME tst_qml_gl_transitions OUTPUTSPEC 15:320x240 QMLFILE gl-transitions.qml OUTPUTFILE gl-transitions.nut THRESHOLD 98.999) +add_qml_test(NAME tst_qml_transformer OUTPUTSPEC 15:320x240 QMLFILE transformer.qml OUTPUTFILE transformer.nut THRESHOLD 99.999) # 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 tst_qml_sequence tst_qml_gl_transitions PROPERTIES LABELS GPU) \ No newline at end of file diff --git a/tests/fixtures b/tests/fixtures index 0ab6a3c..c737deb 160000 --- a/tests/fixtures +++ b/tests/fixtures @@ -1 +1 @@ -Subproject commit 0ab6a3cf9b258b598d31c99f6f765b684360d0e0 +Subproject commit c737debc0fbd080b1b587057d07dd872091cf686 diff --git a/tests/qml/transformer.qml b/tests/qml/transformer.qml new file mode 100644 index 0000000..ec9b651 --- /dev/null +++ b/tests/qml/transformer.qml @@ -0,0 +1,59 @@ +// Copyright (C) 2024 Andrew Wason +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtMultimedia +import MediaFX +import MediaFX.Transition.GL + +Item { + MediaSequence { + id: sequence + + anchors.fill: parent + + Component.onCompleted: { + sequence.mediaSequenceEnded.connect(sequence.RenderSession.session.endSession); + } + + Component { + MediaSequenceClip { + id: clip1 + endTime: 3500 + source: Qt.resolvedUrl("../fixtures/assets/road.jpg") + endTransition: Bounce { + objectName: "Bounce" + duration: 1000 + } + transformer: Transformer { + NumberAnimation on scale { + to: 2 + duration: clip1.duration + } + PropertyAnimation on translate { + to: Qt.point(0.5, 0.5) + duration: clip1.duration + } + } + } + } + Component { + MediaSequenceClip { + id: clip2 + endTime: 3500 + source: Qt.resolvedUrl("../fixtures/assets/lake.jpg") + transformer: Transformer { + NumberAnimation on scale { + to: 2 + duration: clip2.duration + } + RotationAnimation on rotate { + to: 45 + easing.type: Easing.InQuart + duration: clip2.duration + } + } + } + } + } +}