Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85587899
D329.1784111655.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
26 KB
Referenced Files
None
Subscribers
None
D329.1784111655.diff
View Options
diff --git a/CMakeLists.txt b/CMakeLists.txt
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -67,6 +67,7 @@
find_package(KF${KF_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS Kirigami Config KIO Notifications CoreAddons ${kazv_KF_EXTRA_MODULES})
find_package(KF6KirigamiAddons)
+find_package(MpvQt REQUIRED)
find_package(Threads REQUIRED)
find_package(nlohmann_json REQUIRED)
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -71,6 +71,7 @@
- nlohmann_json
- libkazv
- cmark
+- mpvqt
## Build process
diff --git a/packaging/GNU-Linux/appimage/build.sh b/packaging/GNU-Linux/appimage/build.sh
--- a/packaging/GNU-Linux/appimage/build.sh
+++ b/packaging/GNU-Linux/appimage/build.sh
@@ -56,6 +56,7 @@
libqt6sql6-sqlite
libqcoro6core0t64
qcoro-qt6-dev
+ mpvqt-dev
# https://develop.kde.org/docs/apps/tests/appium/#building-manually
libkf6windowsystem-dev
libwayland-dev
diff --git a/packaging/GNU-Linux/flatpak/moe.kazv.mxc.kazv.yaml b/packaging/GNU-Linux/flatpak/moe.kazv.mxc.kazv.yaml
--- a/packaging/GNU-Linux/flatpak/moe.kazv.mxc.kazv.yaml
+++ b/packaging/GNU-Linux/flatpak/moe.kazv.mxc.kazv.yaml
@@ -32,6 +32,36 @@
- type: archive
url: "https://github.com/weidai11/cryptopp/archive/refs/tags/CRYPTOPP_8_9_0.tar.gz"
sha256: ab5174b9b5c6236588e15a1aa1aaecb6658cdbe09501c7981ac8db276a24d9ab
+ - name: mpv
+ buildsystem: meson
+ config-opts:
+ - -Drubberband=disabled
+ - -Dlibmpv=true
+ sources:
+ - type: archive
+ url: 'https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz'
+ sha256: ee21092a5ee427353392360929dc64645c54479aefdb5babc5cfbb5fad626209
+ modules:
+ - name: libplacebo
+ buildsystem: meson
+ config-opts:
+ - -Ddemos=False
+ sources:
+ - type: git
+ url: 'https://code.videolan.org/videolan/libplacebo'
+ commit: cee9b076f2c63104ccfd497fa79c39a867293ec4 # 7.360.1
+ - name: libass
+ buildsystem: meson
+ sources:
+ - type: archive
+ url: https://github.com/libass/libass/releases/download/0.17.4/libass-0.17.4.tar.gz
+ sha256: a886b3b80867f437bc55cff3280a652bfa0d37b43d2aff39ddf3c4f288b8c5a8
+ - name: mpvqt
+ buildsystem: cmake-ninja
+ sources:
+ - type: archive
+ url: 'https://invent.kde.org/libraries/mpvqt/-/archive/v1.2.0/mpvqt-v1.2.0.tar.gz?ref_type=tags'
+ sha256: 882f4f4a4114ba3c7b13cdae6e09c8cc151c73ace0acbcdc31c1b7039a492db3
- buildsystem: simple
name: vodozemac
build-commands:
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -75,6 +75,7 @@
matrix-verification-list.cpp
kazv-abstract-list-model.cpp
qt-promise-handler.cpp
+ mpv-media-player.cpp
device-mgmt/matrix-device.cpp
device-mgmt/matrix-device-list.cpp
@@ -122,6 +123,7 @@
KF${KF_MAJOR_VERSION}::Notifications
KF${KF_MAJOR_VERSION}::CoreAddons
QCoro::Core
+ MpvQt::MpvQt
${CMARK_TARGET_NAME}
)
@@ -219,6 +221,8 @@
SelectableText.qml
ReactToEventPopup.qml
FileHandler.qml
+ media/VideoWithControl.qml
+ media/PlayButton.qml
ActionSettingsPage.qml
CreateRoomPage.qml
diff --git a/src/contents/ui/event-types/Video.qml b/src/contents/ui/event-types/Video.qml
--- a/src/contents/ui/event-types/Video.qml
+++ b/src/contents/ui/event-types/Video.qml
@@ -14,6 +14,7 @@
import '.' as Types
import '..' as Kazv
+import '../media' as M
Simple {
id: upper
@@ -22,9 +23,8 @@
property var body: event.content.body
property var videoInfo: event.content.info || {}
- property var videoResolution: video.metaData.value(MediaMetaData.Resolution)
- property var videoWidth: videoResolution && videoResolution.width || (videoInfo.w || 1)
- property var videoHeight: videoResolution && videoResolution.height || (videoInfo.h || 1)
+ property var videoWidth: video.contentWidth || videoInfo.w
+ property var videoHeight: video.contentHeight || videoInfo.h
property var videoMaxHeight: compactMode ? Kirigami.Units.gridUnit * 5 : Infinity
property var thumbnailInfo: videoInfo.thumbnail_info || {}
property var thumbnailMxcUri: videoInfo.thumbnail_url
@@ -68,70 +68,31 @@
text: l10n.get('event-message-video-sent', { gender, body })
visible: !compactMode
}
- property var video: Video {
+ property var video: M.VideoWithControl {
id: video
objectName: 'video'
source: fileHandler.kazvIOJob ? "" : fileHandler.cachedFile
- autoPlay: !hasAudio
- loops: MediaPlayer.Infinite
Layout.fillWidth: true
- property double videoWidth: upper.thumbnailWidth || upper.videoWidth || video.implicitWidth || 1
- property double videoHeight: upper.thumbnailHeight || upper.videoHeight || video.implicitHeight || 1
+ property double videoWidth: Math.max(upper.thumbnailWidth || upper.videoWidth || video.implicitWidth || 1, Kirigami.Units.gridUnit)
+ property double videoHeight: Math.max(upper.thumbnailHeight || upper.videoHeight || video.implicitHeight || 1, Kirigami.Units.gridUnit)
property double privHeight: Math.min(videoMaxHeight, videoHeight)
property double privWidth: privHeight / videoHeight * videoWidth
Layout.preferredWidth: privWidth
Layout.preferredHeight: width < privWidth ? privHeight / privWidth * width : privHeight
- fillMode: VideoOutput.PreserveAspectFit
- TapHandler {
- onSingleTapped: {
- video.playOrPause();
- }
- }
-
- Image {
- anchors.fill: parent
- source: thumbnailUri
- visible: video.playbackState == MediaPlayer.StoppedState && hasThumbnail
- fillMode: Image.PreserveAspectFit
- data: [bubble.mediaFileMenu]
- }
-
- Rectangle {
+ Loader {
anchors.fill: parent
- color: Kirigami.Theme.negativeBackgroundColor //Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
- visible: {
- video.playbackState == MediaPlayer.StoppedState &&
- ! hasThumbnail
- }
- }
-
- Control {
- anchors.centerIn: parent
- height: Math.min(video.height, video.width, Kirigami.Units.iconSizes.large)
- width: Math.min(video.height, video.width, Kirigami.Units.iconSizes.large)
- visible: video.playbackState !== MediaPlayer.PlayingState
-
- background: Rectangle {
- radius: width / 2
- color: Qt.alpha(Kirigami.Theme.backgroundColor, 0.4)
+ property Component thumbnail: Image {
+ source: thumbnailUri
+ fillMode: Image.PreserveAspectFit
+ data: [bubble.mediaFileMenu]
}
- contentItem: Kirigami.Icon {
- source: 'media-playback-start'
+ property Component cover: Rectangle {
+ color: Kirigami.Theme.negativeBackgroundColor //Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
}
- }
- function playOrPause() {
- console.log('playback state ==', video.playbackState);
- if (video.playbackState == MediaPlayer.PlayingState) {
- video.pause();
- } else {
- if (video.playbackState == MediaPlayer.StoppedState) {
- video.seek(0);
- }
- video.play();
- }
+ sourceComponent: (!video.stopped) ? undefined : (upper.hasThumbnail ? thumbnail : cover)
}
}
data: [
diff --git a/src/contents/ui/media/PlayButton.qml b/src/contents/ui/media/PlayButton.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/media/PlayButton.qml
@@ -0,0 +1,52 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import QtQuick.Shapes
+import '..' as Kazv
+
+import org.kde.kirigami as Kirigami
+
+AbstractButton {
+ id: button
+ focusPolicy: Qt.StrongFocus
+
+ background: Kazv.FocusRingBackground {
+ target: button
+ radius: width / 2
+ }
+
+ Rectangle {
+ id: r
+ anchors.centerIn: parent
+ implicitWidth: Kirigami.Units.iconSizes.large
+ implicitHeight: Kirigami.Units.iconSizes.large
+ radius: width / 2
+ color: Qt.alpha(Kirigami.Theme.backgroundColor, 0.4)
+ Shape {
+ /*
+ * Center a triangle inside a circle
+ */
+ id: s
+ property real w: r.width
+ property real t: r.width / 5
+
+ ShapePath {
+ strokeColor: 'transparent'
+ fillColor: Qt.alpha(Kirigami.Theme.textColor, 0.8)
+ PathPolyline {
+ path: [
+ Qt.point(s.w/2 - s.t/Math.sqrt(3), s.w/2 - s.t),
+ Qt.point(s.w/2 - s.t/Math.sqrt(3), s.w/2 + s.t),
+ Qt.point((Math.sqrt(3) - 1 / Math.sqrt(3))*s.t + s.w/2, s.w/2),
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/src/contents/ui/media/VideoWithControl.qml b/src/contents/ui/media/VideoWithControl.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/media/VideoWithControl.qml
@@ -0,0 +1,62 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import '..' as Kazv
+import moe.kazv.mxc.kazv as MK
+import org.kde.kirigami as Kirigami
+
+MK.MpvMediaPlayer {
+ id: video
+ autoPlayPolicy: MK.MpvMediaPlayer.AutoPlayIfNoSound
+
+ onReady: console.log('ready');
+
+ PlayButton {
+ id: playButton
+ z: 10
+ anchors.centerIn: parent
+ height: Math.min(video.height, video.width, Kirigami.Units.iconSizes.large)
+ width: Math.min(video.height, video.width, Kirigami.Units.iconSizes.large)
+ states: [
+ State {
+ name: 'shown'
+ when: !video.playing
+ PropertyChanges {
+ target: playButton
+ visible: true
+ opacity: 1
+ }
+ },
+ State {
+ name: 'hidden'
+ when: video.playing
+ PropertyChanges {
+ target: playButton
+ opacity: 0
+ visible: false
+ }
+ }
+ ]
+ transitions: [
+ Transition {
+ NumberAnimation {
+ properties: "opacity,visible"
+ duration: Kirigami.Units.shortDuration
+ }
+ }
+ ]
+ onClicked: video.togglePlaying();
+ }
+
+ TapHandler {
+ onSingleTapped: {
+ video.togglePlaying();
+ }
+ }
+}
diff --git a/src/mpv-media-player.hpp b/src/mpv-media-player.hpp
new file mode 100644
--- /dev/null
+++ b/src/mpv-media-player.hpp
@@ -0,0 +1,82 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <kazv-defs.hpp>
+#include <MpvAbstractItem>
+#include <QUrl>
+#include <memory>
+
+class MpvMediaPlayer : public MpvAbstractItem
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ enum AutoPlayPolicy
+ {
+ /// Never auto play
+ AutoPlayDisabled,
+ /// Auto play if the media has no audio track
+ AutoPlayIfNoSound,
+ };
+
+ Q_ENUM(AutoPlayPolicy)
+
+private:
+ Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
+ Q_PROPERTY(bool playing READ isPlaying NOTIFY playingChanged)
+ Q_PROPERTY(bool stopped READ isStopped NOTIFY stoppedChanged)
+ Q_PROPERTY(qreal contentWidth READ contentWidth NOTIFY contentWidthChanged)
+ Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged)
+ Q_PROPERTY(AutoPlayPolicy autoPlayPolicy READ autoPlayPolicy WRITE setAutoPlayPolicy NOTIFY autoPlayPolicyChanged)
+
+public:
+ MpvMediaPlayer(QQuickItem *parent = nullptr);
+ ~MpvMediaPlayer() override;
+
+ QUrl source() const;
+
+ void setSource(const QUrl &newSource);
+
+ bool isPlaying() const;
+
+ bool isStopped() const;
+
+ AutoPlayPolicy autoPlayPolicy() const;
+
+ void setAutoPlayPolicy(AutoPlayPolicy autoPlayPolicy);
+
+ qreal contentWidth() const;
+
+ qreal contentHeight() const;
+
+Q_SIGNALS:
+ void sourceChanged();
+ void contentWidthChanged();
+ void contentHeightChanged();
+ void playingChanged();
+ void stoppedChanged();
+ void autoPlayPolicyChanged();
+
+public Q_SLOTS:
+ void startPlaying();
+ void pausePlaying();
+ void togglePlaying();
+
+private Q_SLOTS:
+ void handlePropertyChange(const QString &property, const QVariant &value);
+ void maybeStartPlaying();
+ void applyPendingSource();
+ void maybeAutoPlayByTrackList(const QVariantList &trackList);
+
+private:
+ void setPlayingState(bool playing);
+ void setStoppedState(bool stopped);
+
+ struct Private;
+ std::unique_ptr<Private> m_d;
+};
diff --git a/src/mpv-media-player.cpp b/src/mpv-media-player.cpp
new file mode 100644
--- /dev/null
+++ b/src/mpv-media-player.cpp
@@ -0,0 +1,218 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <kazv-defs.hpp>
+#include "mpv-media-player.hpp"
+#include <MpvController>
+
+using namespace Qt::Literals::StringLiterals;
+
+QString urlToMpvFilename(const QUrl &url)
+{
+ if (url.isLocalFile()) {
+ return url.toLocalFile();
+ }
+ return QString();
+}
+
+struct MpvMediaPlayer::Private
+{
+ QUrl source;
+ bool playing{false};
+ bool stopped{true};
+ bool ready{false};
+ bool waitingForTrackList{false};
+ std::optional<QUrl> pendingSource;
+ int contentWidth{0};
+ int contentHeight{0};
+ AutoPlayPolicy autoPlayPolicy{AutoPlayDisabled};
+};
+
+MpvMediaPlayer::MpvMediaPlayer(QQuickItem *parent)
+ : MpvAbstractItem(parent)
+ , m_d(std::make_unique<Private>())
+{
+ setFocusPolicy(Qt::StrongFocus);
+ setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);
+ setAcceptHoverEvents(true);
+ observeProperty(u"width"_s, MPV_FORMAT_INT64);
+ observeProperty(u"height"_s, MPV_FORMAT_INT64);
+ observeProperty(u"pause"_s, MPV_FORMAT_STRING);
+ // observeProperty(u"playlist/0/filename"_s, MPV_FORMAT_STRING);
+ observeProperty(u"track-list"_s, MPV_FORMAT_NODE);
+ setProperty(u"loop-playlist"_s, u"inf"_s);
+ setProperty(u"border-background"_s, u"none"_s);
+ setProperty(u"background-color"_s, u"0/0"_s);
+
+ connect(mpvController(), &MpvController::propertyChanged,
+ this, &MpvMediaPlayer::handlePropertyChange);
+ connect(mpvController(), &MpvController::fileLoaded,
+ this, &MpvMediaPlayer::maybeStartPlaying);
+ connect(this, &MpvMediaPlayer::ready,
+ this, &MpvMediaPlayer::applyPendingSource);
+}
+
+MpvMediaPlayer::~MpvMediaPlayer() = default;
+
+QUrl MpvMediaPlayer::source() const
+{
+ return m_d->source;
+}
+
+void MpvMediaPlayer::setSource(const QUrl &newSource)
+{
+ if (!m_d->ready) {
+ m_d->pendingSource = newSource;
+ return;
+ }
+
+ if (newSource == m_d->source) {
+ return;
+ }
+
+ m_d->source = newSource;
+ command({u"stop"_s});
+ // Start the file in paused state
+ // mpv will consider the file to be "paused" and "current" and "playing"
+ setProperty(u"pause"_s, u"yes"_s);
+ if (!newSource.isEmpty()) {
+ command({u"loadfile"_s, urlToMpvFilename(newSource), u"replace"_s});
+ } else {
+ setStoppedState(true);
+ }
+ Q_EMIT sourceChanged();
+}
+
+void MpvMediaPlayer::handlePropertyChange(const QString &property, const QVariant &value)
+{
+ if (property == u"width"_s) {
+ setImplicitWidth(value.toReal());
+ m_d->contentWidth = value.toReal();
+ Q_EMIT contentWidthChanged();
+ } else if (property == u"height"_s) {
+ setImplicitHeight(value.toReal());
+ m_d->contentHeight = value.toReal();
+ Q_EMIT contentHeightChanged();
+ } else if (property == u"pause"_s) {
+ setPlayingState(!(value == u"yes"_s));
+ } else if (property == u"track-list"_s) {
+ if (m_d->waitingForTrackList) {
+ auto trackList = value.toList();
+ if (!trackList.isEmpty()) {
+ m_d->waitingForTrackList = false;
+ maybeAutoPlayByTrackList(trackList);
+ }
+ }
+ }
+}
+
+void MpvMediaPlayer::startPlaying()
+{
+ setStoppedState(false);
+ setProperty(u"pause"_s, u"no"_s);
+}
+
+void MpvMediaPlayer::pausePlaying()
+{
+ setProperty(u"pause"_s, u"yes"_s);
+}
+
+void MpvMediaPlayer::togglePlaying()
+{
+ if (!isPlaying()) {
+ startPlaying();
+ } else {
+ pausePlaying();
+ }
+}
+
+void MpvMediaPlayer::maybeStartPlaying()
+{
+ auto v = getProperty(u"track-list"_s).toList();
+ if (v.isEmpty()) {
+ m_d->waitingForTrackList = true;
+ return;
+ }
+
+ maybeAutoPlayByTrackList(v);
+}
+
+void MpvMediaPlayer::maybeAutoPlayByTrackList(const QVariantList &trackList)
+{
+ switch (m_d->autoPlayPolicy) {
+ case AutoPlayDisabled:
+ break;
+
+ case AutoPlayIfNoSound:
+ if (std::none_of(trackList.begin(), trackList.end(), [](const QVariant &trackV) {
+ const auto track = trackV.toMap();
+ return track.value(u"type"_s) == u"audio"_s;
+ })) {
+ startPlaying();
+ }
+ break;
+
+ default:
+ Q_ASSERT_X(
+ false,
+ "MpvMediaPlayer::maybeStartPlaying",
+ "Invalid autoPlayPolicy value");
+ break;
+ }
+}
+
+bool MpvMediaPlayer::isPlaying() const
+{
+ return m_d->playing;
+}
+
+void MpvMediaPlayer::setPlayingState(bool playing)
+{
+ m_d->playing = playing;
+ Q_EMIT playingChanged();
+}
+
+bool MpvMediaPlayer::isStopped() const
+{
+ return m_d->stopped;
+}
+
+void MpvMediaPlayer::setStoppedState(bool stopped)
+{
+ m_d->stopped = stopped;
+ Q_EMIT stoppedChanged();
+}
+
+auto MpvMediaPlayer::autoPlayPolicy() const -> AutoPlayPolicy
+{
+ return m_d->autoPlayPolicy;
+}
+
+void MpvMediaPlayer::setAutoPlayPolicy(AutoPlayPolicy autoPlayPolicy)
+{
+ m_d->autoPlayPolicy = autoPlayPolicy;
+ Q_EMIT autoPlayPolicyChanged();
+}
+
+qreal MpvMediaPlayer::contentWidth() const
+{
+ return m_d->contentWidth;
+}
+
+qreal MpvMediaPlayer::contentHeight() const
+{
+ return m_d->contentHeight;
+}
+
+void MpvMediaPlayer::applyPendingSource()
+{
+ m_d->ready = true;
+ if (m_d->pendingSource) {
+ auto source = std::move(m_d->pendingSource).value();
+ m_d->pendingSource = std::nullopt;
+ setSource(source);
+ }
+}
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -137,6 +137,7 @@
TestStickerPicker.py
TestEventViewUnavailable.py
TestRoomTimelineView.py
+ TestMpvMediaPlayer.py
)
foreach(testfilename ${appium_tests_SOURCES})
message(STATUS "Adding appium test ${testfilename}")
diff --git a/src/tests/appium-tests/TestMpvMediaPlayer.py b/src/tests/appium-tests/TestMpvMediaPlayer.py
new file mode 100755
--- /dev/null
+++ b/src/tests/appium-tests/TestMpvMediaPlayer.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+# This file is part of kazv.
+# SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+from helpers import utils
+from appium.webdriver.common.appiumby import AppiumBy
+from selenium.webdriver.support.ui import WebDriverWait
+import json
+import os.path
+
+resources_dir = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ 'resources'
+)
+no_audio_file = os.path.join(resources_dir, 'no-sound.webm')
+with_audio_file = os.path.join(resources_dir, 'with-sound.webm')
+
+class MpvMediaPlayerTest(utils.ComponentTest):
+ def test_auto_play_no_audio(self):
+ self.driver.find_element(AppiumBy.NAME, 'file-name').send_keys(f'file://{no_audio_file}')
+ self.driver.find_element(AppiumBy.NAME, 'apply-file').click()
+ self.driver.find_element(AppiumBy.NAME, 'playing=true')
+
+ def test_do_not_auto_play_with_audio(self):
+ self.driver.find_element(AppiumBy.NAME, 'file-name').send_keys(f'file://{with_audio_file}')
+ self.driver.find_element(AppiumBy.NAME, 'apply-file').click()
+ self.driver.find_element(AppiumBy.NAME, 'playing=false')
+
+if __name__ == '__main__':
+ utils.main(__file__)
diff --git a/src/tests/appium-tests/TestMpvMediaPlayer.qml b/src/tests/appium-tests/TestMpvMediaPlayer.qml
new file mode 100644
--- /dev/null
+++ b/src/tests/appium-tests/TestMpvMediaPlayer.qml
@@ -0,0 +1,45 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtTest
+import '../../contents/ui' as Kazv
+import '../../contents/ui/media' as M
+import moe.kazv.mxc.kazv as MK
+import org.kde.kirigami as Kirigami
+import 'qml-helpers' as AppiumQmlHelpers
+
+AppiumQmlHelpers.TestItem {
+ id: upper
+
+ function reset() {
+ fileNameInput.text = '';
+ video.source = '';
+ }
+
+ TextField {
+ id: fileNameInput
+ Accessible.name: 'file-name'
+ }
+
+ Button {
+ text: 'apply-file'
+ onClicked: video.source = fileNameInput.text;
+ }
+
+ AppiumQmlHelpers.InfoLabel {
+ tag: 'playing'
+ value: `${video.playing}`
+ }
+
+ M.VideoWithControl {
+ id: video
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+}
diff --git a/src/tests/resources/no-sound.webm b/src/tests/resources/no-sound.webm
new file mode 100644
index 0000000000000000000000000000000000000000..e004b4478ff3b2b827d7aac2fef11282dc4bda30
GIT binary patch
literal 2163
zc$~GCT}V@59LC?Xv*X5YbWti|6j~@kv23Y@)I8f<1E=C_rZ=TCmzfbx(>^ZtZtf#L
zsEi=yNM#o_{K(7-T*jfO>>@9U)GIATyot;Tx`?D57sT+n?d3fe|L6S9bDsa7!#O8K
zzYy%>J(S^pMviY(%73SF-r3%Is-}_mQVH*-l4P^P#t?DIwMtWO^MonfX&!9fW(?Jt
zhv-1GN-Q5~h*Wc?a95<-QvSkb9&$1%(K6K>j+}JL_k51}kV^8umY8ZRYxaRFp4XTh
z-9^<G>T<I8?u%?yc11yKt-I>e^fzr>CdQu}`I}BXOBwr1Uk|t3+_;=u8K5NXQNY!5
zuGUA1$89-bDLv?-O!|_`wO*R_(KAYiC+M))@|@k?ZxtN2DZf7G)fa0^bow%#H}<A>
z`<PYewEnZVO%3RSKHaghf^uU?vCex_r8*>Tkv^c>SY(IxaZBOo3RMO_Q9>L}ql+gB
zLGenFw<|TdjS#^w_q+Ow+7KeEgq+n)ysOt%O}{a?58LN+w_6kPWoH(Xp83~1GkhUo
zapw9jX8dUFUFN-UXM(@+a+CYRSld0xxFV>2Ep*9)w<u8(57G1lbR`bLo+qR#(?LWz
zEh^_1Yhq+Iru9)-T_nWF7EIf)cN?ZVqH?D3WQ^PkX{IJ33!7-hj%hE<&D@5xWGl=q
z*^TKOm|J4N^l|K6kLgRWOGyCJlh}J2(+fEF8l>5D=*{Bige`>gW-Bm#8g|KE#B>+T
z&Gun>6z6`9=_Q=|2c)GO^p+-KIs<ko&BL@2dskt)0p^xo!E`tFzJuxeaNbe@(=*t6
z1=3tR%+2k`v>tkM7C2bW26J=x?Zf%8_aLU9;@odBJ&(OtAst6x?l}DZmK}lKvQkXf
i!rZbJNXwHp4_5r_-w0oONR>}IBEJ`FPNwxMW&Z$ALxxBI
literal 0
Hc$@<O00001
diff --git a/src/tests/resources/with-sound.webm b/src/tests/resources/with-sound.webm
new file mode 100644
index 0000000000000000000000000000000000000000..631cfb4e4772a12174f59cc7a73c5224cea955d3
GIT binary patch
literal 12576
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
K=!J`P#D4%u%Gj3x
literal 0
Hc$@<O00001
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Jul 15, 3:34 AM (21 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1691326
Default Alt Text
D329.1784111655.diff (26 KB)
Attached To
Mode
D329: Use mpv to display videos
Attached
Detach File
Event Timeline
Log In to Comment