Page MenuHomePhorge

D329.1784111655.diff
No OneTemporary

Size
26 KB
Referenced Files
None
Subscribers
None

D329.1784111655.diff

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

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)

Event Timeline