Skip to content

Commit

Permalink
Support transforming video (pan, zoom, rotate)
Browse files Browse the repository at this point in the history
  • Loading branch information
rectalogic committed Jun 21, 2024
1 parent 2b5cec4 commit f597f1c
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 22 deletions.
1 change: 1 addition & 0 deletions src/MediaFX/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ qt_add_qml_module(mediafx
MediaSequenceClip.qml
MultiEffectState.qml
ShaderEffectState.qml
Transformer.qml
DEPENDENCIES
QtQuick
QtQuick.Controls
Expand Down
40 changes: 32 additions & 8 deletions src/MediaFX/MediaSequence.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}
2 changes: 2 additions & 0 deletions src/MediaFX/MediaSequenceClip.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
42 changes: 42 additions & 0 deletions src/MediaFX/Transformer.qml
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 4 additions & 0 deletions src/MediaFX/render_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ void RenderSession::componentComplete()
m_loadedItem = qobject_cast<QQuickItem*>(object);
if (!m_loadedItem) {
qmlWarning(this) << "Failed to load" << m_sourceUrl;
if (component.isError()) {
for (auto& error : component.errors())
qCritical() << error;
}
fatalError();
return;
}
Expand Down
27 changes: 14 additions & 13 deletions src/MediaFX/sequence.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
};
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion tests/fixtures
59 changes: 59 additions & 0 deletions tests/qml/transformer.qml
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
}
}

0 comments on commit f597f1c

Please sign in to comment.