Change to Qt 6

Co-authored by: Izzie Walton <ian@iwalton.com>
Co-authored by: Satadru Pramanik <satadru@gmail.com>
Co-authored by: sevenrats <sevenrats@tutanota.com>
Co-authored by: xB <65196493+xBiei@users.noreply.github.com>
Co-authored by: zjeffer <4633209+zjeffer@users.noreply.github.com>
This commit is contained in:
Andrew Rabert committed 2025-11-22 18:16:00 -05:00
1 parent 713ac65fab
commit c7e9cf7486
50 files changed
+1594 -850

No files matched your search

+13 -13
View File
@@ -14,13 +14,13 @@ jobs:
- name: Install dependencies
run: |
brew update
brew install ninja mpv qt@5 || true
brew install ninja mpv qt@6 || true
- name: Release build
run: |
mkdir build
sed -i '' 's|await loadScript('\''qrc:///qtwebchannel/qwebchannel.js'\'');|document.body.style.overscrollBehavior = "none";&|g' native/nativeshell.js
cd build
cmake -GNinja -DQTROOT=/usr/local/opt/qt@5 -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=output ..
cmake -GNinja -DQTROOT=/usr/local/opt/qt@6 -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=output ..
ninja install
- name: Fix library paths and create dmg
run: |
@@ -53,13 +53,13 @@ jobs:
- name: Install dependencies
run: |
brew update
brew install ninja mpv qt@5 || true
brew install ninja mpv qt@6 || true
- name: Release build
run: |
mkdir build
sed -i '' 's|await loadScript('\''qrc:///qtwebchannel/qwebchannel.js'\'');|document.body.style.overscrollBehavior = "none";&|g' native/nativeshell.js
cd build
cmake -GNinja -DQTROOT=/opt/homebrew/opt/qt@5 -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=output ..
cmake -GNinja -DQTROOT=/opt/homebrew/opt/qt@6 -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=output ..
ninja install
- name: Fix library paths and create dmg
run: |
@@ -78,10 +78,10 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install Qt 5.15.2
- name: Install Qt 6
uses: jurplel/install-qt-action@v3
with:
version: "5.15.2"
version: "6.8.0"
arch: "win64_msvc2019_64"
modules: "qtwebengine"
- name: Install dependencies
@@ -107,7 +107,7 @@ jobs:
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
cd build
set PATH=%PATH%;C:\Program Files (x86)\WiX Toolset v3.11\bin;%CD%
cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=output -DCMAKE_MAKE_PROGRAM=ninja.exe -DQTROOT=%Qt5_DIR% -DMPV_INCLUDE_DIR=mpv/include -DMPV_LIBRARY=mpv/libmpv-2.dll -DCMAKE_INSTALL_PREFIX=output ..
cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=output -DCMAKE_MAKE_PROGRAM=ninja.exe -DQTROOT=%Qt6_DIR% -DMPV_INCLUDE_DIR=mpv/include -DMPV_LIBRARY=mpv/libmpv-2.dll -DCMAKE_INSTALL_PREFIX=output ..
lib /def:mpv\mpv.def /out:mpv\libmpv-2.dll.lib /MACHINE:X64
ninja
ninja windows_package
@@ -122,10 +122,10 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install Qt 5.15.2
- name: Install Qt 6
uses: jurplel/install-qt-action@v4
with:
version: "5.15.2"
version: "6.8.0"
arch: "win64_msvc2019_64"
modules: "qtwebengine"
- name: Install dependencies
@@ -151,7 +151,7 @@ jobs:
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
cd build
set PATH=%PATH%;C:\Program Files (x86)\WiX Toolset v3.11\bin;%CD%
cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=output -DCMAKE_MAKE_PROGRAM=ninja.exe -DQTROOT=%Qt5_DIR% -DMPV_INCLUDE_DIR=mpv/include -DMPV_LIBRARY=mpv/libmpv-2.dll -DCMAKE_INSTALL_PREFIX=output ..
cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=output -DCMAKE_MAKE_PROGRAM=ninja.exe -DQTROOT=%Qt6_DIR% -DMPV_INCLUDE_DIR=mpv/include -DMPV_LIBRARY=mpv/libmpv-2.dll -DCMAKE_INSTALL_PREFIX=output ..
lib /def:mpv\mpv.def /out:mpv\libmpv-2.dll.lib /MACHINE:X64
ninja
ninja windows_package
@@ -166,10 +166,10 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install Qt 5.15.2
- name: Install Qt 6
uses: jurplel/install-qt-action@v4
with:
version: "5.15.2"
version: "6.8.0"
arch: "win32_msvc2019"
modules: "qtwebengine"
- name: Install dependencies
@@ -195,7 +195,7 @@ jobs:
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars32.bat"
cd build
set PATH=%PATH%;C:\Program Files (x86)\WiX Toolset v3.11\bin;%CD%
cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=output -DCMAKE_MAKE_PROGRAM=ninja.exe -DQTROOT=%Qt5_DIR% -DMPV_INCLUDE_DIR=mpv/include -DMPV_LIBRARY=mpv/libmpv-2.dll -DCMAKE_INSTALL_PREFIX=output ..
cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=output -DCMAKE_MAKE_PROGRAM=ninja.exe -DQTROOT=%Qt6_DIR% -DMPV_INCLUDE_DIR=mpv/include -DMPV_LIBRARY=mpv/libmpv-2.dll -DCMAKE_INSTALL_PREFIX=output ..
lib /def:mpv\mpv.def /out:mpv\libmpv-2.dll.lib /MACHINE:X86
ninja
ninja windows_package
+2 -2
View File
@@ -70,9 +70,9 @@ elseif(UNIX AND (NOT APPLE))
include(LinuxConfiguration)
endif(APPLE)
if (Qt5_POSITION_INDEPENDENT_CODE)
if (Qt6_POSITION_INDEPENDENT_CODE)
SET(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif(Qt5_POSITION_INDEPENDENT_CODE)
endif(Qt6_POSITION_INDEPENDENT_CODE)
add_subdirectory(external)
add_subdirectory(src)
@@ -41,3 +41,4 @@ endif(IS_DIRECTORY ${DEPENDENCY_ROOT})
find_package(Threads REQUIRED)
find_package(PythonInterp REQUIRED)
find_package(MpvQt REQUIRED)
+21 -26
View File
@@ -19,32 +19,27 @@ list(APPEND CMAKE_FIND_ROOT_PATH ${QTROOT})
list(APPEND CMAKE_PREFIX_PATH ${QTROOT})
include_directories(${QTROOT}/include)
set(REQUIRED_QT_VERSION "5.7.0")
set(REQUIRED_QT_VERSION "6.0.0")
set(QTCONFIGROOT ${QTROOT}/lib/cmake/Qt5)
set(components Core Network WebChannel Qml Quick Xml WebChannel WebEngine WebEngineWidgets Widgets)
set(QTCONFIGROOT ${QTROOT}/lib/cmake/Qt6)
set(components Core Network WebChannel Qml Quick Xml WebEngineQuick WebEngineCore Widgets OpenGL)
if(UNIX AND (NOT APPLE) AND ((NOT BUILD_TARGET STREQUAL "RPI")))
add_definitions(-DUSE_X11EXTRAS)
set(components ${components} X11Extras Gui)
set(components ${components} Gui)
endif()
if(LINUX_DBUS)
set(components ${components} DBus)
endif(LINUX_DBUS)
if(WIN32)
set(components ${components} WinExtras)
endif(WIN32)
foreach(COMP ${components})
set(mod Qt5${COMP})
set(mod Qt6${COMP})
# look for the config files in the QtConfigRoot defined above
set(${mod}_DIR ${QTCONFIGROOT}${COMP})
# look for the actual package
find_package(${mod} ${REQUIRED_QT_VERSION} REQUIRED)
find_package(Qt6 REQUIRED COMPONENTS ${COMP})
include_directories(${${mod}_INCLUDE_DIRS})
if(OPENELEC)
@@ -52,26 +47,26 @@ foreach(COMP ${components})
endif(OPENELEC)
# Need private interfaces for qpa/qplatformnativeinterface.h:
if(${mod} STREQUAL Qt5Gui)
include_directories(${Qt5Gui_PRIVATE_INCLUDE_DIRS})
if(${mod} STREQUAL Qt6Gui)
include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS})
endif()
list(APPEND QT5_LIBRARIES ${${mod}_LIBRARIES})
list(APPEND QT5_CFLAGS ${${mod}_EXECUTABLE_COMPILE_FLAGS})
list(APPEND QT6_LIBRARIES ${${mod}_LIBRARIES})
list(APPEND QT6_CFLAGS ${${mod}_EXECUTABLE_COMPILE_FLAGS})
endforeach(COMP ${components})
if(QT5_CFLAGS)
list(REMOVE_DUPLICATES QT5_CFLAGS)
if(QT6_CFLAGS)
list(REMOVE_DUPLICATES QT6_CFLAGS)
if(WIN32)
list(REMOVE_ITEM QT5_CFLAGS -fPIC)
list(REMOVE_ITEM QT6_CFLAGS -fPIC)
endif(WIN32)
endif(QT5_CFLAGS)
endif(QT6_CFLAGS)
message(STATUS "Qt version: ${Qt5Core_VERSION_STRING}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${QT5_CFLAGS}")
message(STATUS "Qt version: ${Qt6Core_VERSION}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${QT6_CFLAGS}")
set(CMAKE_REQUIRED_INCLUDES ${Qt5WebEngine_INCLUDE_DIRS};${Qt5WebEngine_PRIVATE_INCLUDE_DIRS})
set(CMAKE_REQUIRED_LIBRARIES ${QT5_LIBRARIES})
set(CMAKE_REQUIRED_INCLUDES ${Qt6WebEngineCore_INCLUDE_DIRS};${Qt6WebEngineCore_PRIVATE_INCLUDE_DIRS})
set(CMAKE_REQUIRED_LIBRARIES ${QT6_LIBRARIES})
include(CheckCXXSourceCompiles)
@@ -83,9 +78,9 @@ CHECK_CXX_SOURCE_COMPILES(
QSurfaceFormat::FormatOption o = QSurfaceFormat::UseOptimalOrientation;
return 0;
}
" QT5_HAVE_OPTIMALORIENTATION)
" QT6_HAVE_OPTIMALORIENTATION)
if(QT5_HAVE_OPTIMALORIENTATION)
if(QT6_HAVE_OPTIMALORIENTATION)
message(STATUS "QSurfaceFormat::UseOptimalOrientation found")
add_definitions(-DHAVE_OPTIMALORIENTATION)
endif()
+5 -9
View File
@@ -31,11 +31,7 @@ git clone https://github.com/jellyfin/jellyfin-media-player.git --branch v1.9.1
Install dependencies:
```bash
sudo apt install build-essential autoconf automake libtool libharfbuzz-dev libfreetype6-dev libfontconfig1-dev libx11-dev libxrandr-dev libvdpau-dev libva-dev mesa-common-dev libegl1-mesa-dev yasm libasound2-dev libpulse-dev libuchardet-dev zlib1g-dev libfribidi-dev git libgnutls28-dev libgl1-mesa-dev libsdl2-dev cmake wget meson nasm ninja-build python3 g++ qtwebengine5-dev qtquickcontrols2-5-dev libqt5x11extras5-dev libcec-dev qml-module-qtquick-controls qml-module-qtwebengine qml-module-qtwebchannel qtbase5-private-dev curl unzip
```
Build commands for Ubuntu:
```bash
sudo apt install autoconf automake build-essential cmake curl g++ git libasound2-dev libcec-dev libegl1-mesa-dev libfontconfig1-dev libfreetype6-dev libfribidi-dev libgl1-mesa-dev libgnutls28-dev libharfbuzz-dev libpulse-dev libsdl2-dev libtool libuchardet-dev libva-dev libvdpau-dev libx11-dev libxrandr-dev mesa-common-dev meson python3 qml6-module-qtqml-workerscript qml6-module-qtquick-controls qml6-module-qtquick-templates qml6-module-qtquick-window qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtwebengine-controlsdelegates qml6-module-qtwebview qt6-base-private-dev qt6-webengine-private-dev unzip wget yasm zlib1g-dev
mkdir ~/jmp; cd ~/jmp
git clone https://github.com/mpv-player/mpv-build.git
cd mpv-build
@@ -62,7 +58,7 @@ rm -rf ~/jmp/
Install dependencies:
```bash
sudo dnf install autoconf automake libtool freetype-devel libXrandr-devel libvdpau-devel libva-devel mesa-libGL-devel libdrm-devel libX11-devel mesa-libEGL-devel yasm alsa-lib pulseaudio-libs-devel zlib-devel fribidi-devel git gnutls-devel mesa-libGLU-devel SDL2-devel cmake wget python g++ qt-devel libcec-devel qt5-qtbase-devel curl unzip qt5-qtwebchannel-devel qt5-qtwebengine-devel qt5-qtx11extras-devel mpv.x86_64 qwt-qt5-devel.x86_64 qt5-qtbase.x86_64 meson.noarch ninja-build.x86_64 qt5-qtbase-private-devel mpv-libs.x86_64 mpv-devel qt5-qtquickcontrols qt5-qtquickcontrols2 nasm libatomic libshaderc-2024.3
sudo dnf install autoconf automake libtool freetype-devel libXrandr-devel libvdpau-devel libva-devel mesa-libGL-devel libdrm-devel libX11-devel mesa-libEGL-devel yasm alsa-lib pulseaudio-libs-devel zlib-devel fribidi-devel git gnutls-devel mesa-libGLU-devel SDL2-devel cmake wget python g++ qt-devel libcec-devel qt6-qtbase-devel curl unzip qt6-qtwebchannel-devel qt6-qtwebengine-devel mpv.x86_64 qt6-qtbase.x86_64 meson.noarch ninja-build.x86_64 qt6-qtbase-private-devel mpv-libs.x86_64 mpv-devel qt6-qtdeclarative nasm libatomic libshaderc-2024.3
```
Build commands for Fedora:
@@ -99,7 +95,7 @@ Please install:
- Place this in the build directory.
- [QT](https://www.qt.io/download-thank-you?hsLang=en)
- This package is huge. You also need to make a QT account...
- Check "MSVC 2019 64-bit" and "Qt WebEngine" under QT 5.15.2.
- Check "MSVC 2019 64-bit" and "Qt WebEngine" under Qt 6.
- [VS2019 Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019)
- Again this will use a lot of disk space. The installer is small though.
- [libmpv1](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/)
@@ -121,7 +117,7 @@ Open the "x86_x64 Cross Tools Command Prompt for VS 2019". `cd` to the `build` d
```
set PATH=%PATH%;C:\Program Files (x86)\WiX Toolset v3.11\bin
cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=output -DCMAKE_MAKE_PROGRAM=ninja.exe -DQTROOT=C:/Qt/5.15.2/msvc2019_64 -DMPV_INCLUDE_DIR=mpv/include -DMPV_LIBRARY=mpv/libmpv-2.dll -DCMAKE_INSTALL_PREFIX=output ..
cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=output -DCMAKE_MAKE_PROGRAM=ninja.exe -DQTROOT=C:/Qt/6.x.x/msvc2019_64 -DMPV_INCLUDE_DIR=mpv/include -DMPV_LIBRARY=mpv/libmpv-2.dll -DCMAKE_INSTALL_PREFIX=output ..
lib /def:mpv\mpv.def /out:mpv\mpv.dll.lib /MACHINE:X64
ninja
ninja windows_package
@@ -129,7 +125,7 @@ ninja windows_package
## Building for MacOS
Install [QT 5.15.2](https://www.qt.io/download-thank-you?hsLang=en), remember to check `Qt WebEngine`.
Install [Qt 6](https://www.qt.io/download-thank-you?hsLang=en), remember to check `Qt WebEngine`.
Then run the following commands (replace <QT_DIR> with your QT installation location):
+6
View File
@@ -1,3 +1,9 @@
jellyfin-media-player (1.13.0-1) unstable; urgency=medium
* Update to Qt 6
-- Izzie Walton <izzie@iwalton.com> Fri, 22 Nov 2024 00:00:00 -0500
jellyfin-media-player (1.12.0-1) unstable; urgency=medium
* Prevent issue where wayland has black screen due to broken gpu accel (#165, #610)
+7 -7
View File
@@ -29,14 +29,14 @@ Build-Depends: debhelper (>= 10),
wget,
python3,
g++,
qtwebengine5-dev,
qtquickcontrols2-5-dev,
libqt5x11extras5-dev,
qt6-webengine-dev,
qt6-declarative-dev,
qt6-base-dev,
libcec-dev,
libmpv-dev,
wget,
unzip,
qtbase5-private-dev
qt6-base-private-dev
Standards-Version: 3.9.4
Homepage: https://jellyfin.org/
Vcs-Git: https://github.org/jellyfin/jellyfin-media-player.git
@@ -45,8 +45,8 @@ Vcs-Browser: https://github.org/jellyfin/jellyfin-media-player
Package: jellyfin-media-player
Architecture: any
Depends: ${shlibs:Depends},
qml-module-qtwebengine,
qml-module-qtwebchannel,
qml-module-qtquick-controls
qml6-module-qtwebengine,
qml6-module-qtwebchannel,
qml6-module-qtquick-controls
Description: Jellyfin is the Free Software Media System.
This package provides the Jellyfin desktop media player.
+4
View File
@@ -7,3 +7,7 @@ add_library(plistparser STATIC
plistserializer.h
)
std_target_properties(plistparser)
target_link_libraries(plistparser
Qt6::Core
Qt6::Xml
)
+1 -1
View File
@@ -272,7 +272,7 @@ after less than a second.
### Client API Usage
The API is a [QWebChannel](https://doc.qt.io/qt-5/qtwebchannel-javascript.html) API which is async and callback-driven.
The API is a [QWebChannel](https://doc.qt.io/qt-6/qtwebchannel-javascript.html) API which is async and callback-driven.
To invoke a function:
+17 -1
View File
@@ -14,7 +14,7 @@ const remap = {
}
class jmpInputPlugin {
constructor({ inputManager }) {
constructor({ inputManager, playbackManager }) {
this.name = 'JMP Input Plugin';
this.type = 'input';
this.id = 'jmpInputPlugin';
@@ -31,6 +31,22 @@ class jmpInputPlugin {
});
});
// Listen for fullscreen setting changes and trigger fullscreenchange event
let lastFullscreenState = window.jmpInfo.settings.main.fullscreen;
window.jmpInfo.settingsUpdate.push(function(section) {
if (section === 'main') {
const currentFullscreenState = window.jmpInfo.settings.main.fullscreen;
if (currentFullscreenState !== lastFullscreenState) {
lastFullscreenState = currentFullscreenState;
console.log('[jmpInputPlugin] Fullscreen setting changed, triggering fullscreenchange event');
const currentPlayer = playbackManager._currentPlayer;
if (currentPlayer) {
window.Events.trigger(currentPlayer, 'fullscreenchange');
}
}
}
});
api.system.hello("jmpInputPlugin");
})();
}
+150 -117
View File
@@ -32,6 +32,10 @@
this.syncPlayWrapAs = 'htmlvideoplayer';
this.priority = -1;
this.useFullSubtitleUrls = true;
/**
* @type {boolean}
*/
this.isLocalPlayer = true;
/**
* @type {boolean}
*/
@@ -130,16 +134,6 @@
this.events.trigger(this, 'timeupdate');
};
/**
* @private
*/
this.onNavigatedToOsd = () => {
const dlg = this._videoDialog;
if (dlg) {
dlg.style.zIndex = 'unset';
}
};
/**
* @private
*/
@@ -154,15 +148,23 @@
this.setPlaybackRate(this.getPlaybackRate());
if (this._currentPlayOptions.fullscreen) {
this.appRouter.showVideoOsd().then(this.onNavigatedToOsd);
} else {
this.setTransparency('backdrop');
this._videoDialog.dlg.style.zIndex = 'unset';
// Hide backdrop when playback starts
const dlg = this._videoDialog;
if (dlg) {
dlg.style.backgroundImage = '';
}
// Need to override default style.
this._videoDialog.style.setProperty('background', 'transparent', 'important');
// Navigate to OSD view to show playback screen
if (this._currentPlayOptions.fullscreen) {
this.appRouter.showVideoOsd();
// Lower video dialog z-index so OSD can receive input
if (dlg) {
dlg.style.zIndex = 'unset';
}
}
// Keep video fullscreen - native OSD is above it
window.api.player.setVideoRectangle(0, 0, 0, 0);
}
if (this._paused) {
@@ -234,7 +236,9 @@
this._currentTime = null;
this.resetSubtitleOffset();
this.loading.show();
if (options.fullscreen) {
this.loading.show();
}
window.api.power.setScreensaverEnabled(false);
const elem = await this.createMediaElement(options);
return await this.setCurrentSrc(elem, options);
@@ -244,6 +248,29 @@
return this.appSettings.get('volume') || 1;
}
tryGetFramerate(options) {
if (options.mediaSource && options.mediaSource.MediaStreams) {
for (let stream of options.mediaSource.MediaStreams) {
if (stream.Type == "Video") {
return stream.RealFrameRate || stream.AverageFrameRate || null;
}
}
}
}
/**
* @private
*/
getStreamByIndex(mediaStreams, jellyIndex) {
for (const stream of mediaStreams) {
if (stream.Index == jellyIndex) {
return stream;
}
}
return null;
}
/**
* @private
*/
@@ -253,98 +280,14 @@
if (source.Type != streamType || source.IsExternal) {
continue;
}
if (source.Index == jellyIndex) {
return relIndex;
}
relIndex += 1;
}
return null;
}
/**
* @private
*/
getStreamByIndex(mediaStreams, jellyIndex) {
for (const source of mediaStreams) {
if (source.Index == jellyIndex) {
return source;
}
}
return null;
}
/**
* @private
*/
getSubtitleParam() {
const options = this._currentPlayOptions;
if (this._subtitleTrackIndexToSetOnPlaying != null && this._subtitleTrackIndexToSetOnPlaying >= 0) {
const initialSubtitleStream = this.getStreamByIndex(options.mediaSource.MediaStreams, this._subtitleTrackIndexToSetOnPlaying);
if (!initialSubtitleStream || initialSubtitleStream.DeliveryMethod === 'Encode') {
this._subtitleTrackIndexToSetOnPlaying = -1;
} else if (initialSubtitleStream.DeliveryMethod === 'External') {
return '#,' + initialSubtitleStream.DeliveryUrl;
}
}
if (this._subtitleTrackIndexToSetOnPlaying == -1 || this._subtitleTrackIndexToSetOnPlaying == null) {
return '';
}
const subtitleRelIndex = this.getRelativeIndexByType(
options.mediaSource.MediaStreams,
this._subtitleTrackIndexToSetOnPlaying,
'Subtitle'
);
return subtitleRelIndex != null
? '#' + subtitleRelIndex
: '';
}
/**
* @private
*/
getAudioParam() {
const options = this._currentPlayOptions;
if (this._audioTrackIndexToSetOnPlaying != null && this._audioTrackIndexToSetOnPlaying >= 0) {
const initialAudioStream = this.getStreamByIndex(options.mediaSource.MediaStreams, this._audioTrackIndexToSetOnPlaying);
if (!initialAudioStream) {
return '#1';
}
}
if (this._audioTrackIndexToSetOnPlaying == -1 || this._audioTrackIndexToSetOnPlaying == null) {
return '#1';
}
const audioRelIndex = this.getRelativeIndexByType(
options.mediaSource.MediaStreams,
this._audioTrackIndexToSetOnPlaying,
'Audio'
);
return audioRelIndex != null
? '#' + audioRelIndex
: '#1';
}
tryGetFramerate(options) {
if (options.mediaSource && options.mediaSource.MediaStreams) {
for (let stream of options.mediaSource.MediaStreams) {
if (stream.Type == "Video") {
return stream.RealFrameRate || stream.AverageFrameRate || null;
}
}
}
}
/**
* @private
*/
@@ -358,7 +301,10 @@
const ms = (options.playerStartPositionTicks || 0) / 10000;
this._currentPlayOptions = options;
this._subtitleTrackIndexToSetOnPlaying = options.mediaSource.DefaultSubtitleStreamIndex == null ? -1 : options.mediaSource.DefaultSubtitleStreamIndex;
this._audioTrackIndexToSetOnPlaying = options.playMethod === 'Transcode' ? null : options.mediaSource.DefaultAudioStreamIndex;
this._audioTrackIndexToSetOnPlaying = options.mediaSource.DefaultAudioStreamIndex;
console.log('[MPV] Audio track index:', this._audioTrackIndexToSetOnPlaying);
console.log('[MPV] Subtitle track index:', this._subtitleTrackIndexToSetOnPlaying);
const streamdata = {type: 'video', headers: {'User-Agent': 'JellyfinMediaPlayer'}, metadata: options.item, media: {}};
const fps = this.tryGetFramerate(options);
@@ -367,18 +313,69 @@
}
const player = window.api.player;
const streams = options.mediaSource?.MediaStreams || [];
// Handle audio
const audioRelIndex = this._audioTrackIndexToSetOnPlaying != null && this._audioTrackIndexToSetOnPlaying >= 0
? this.getRelativeIndexByType(streams, this._audioTrackIndexToSetOnPlaying, 'Audio')
: -1;
// Handle subtitle - check for external first
let subtitleParam;
if (this._subtitleTrackIndexToSetOnPlaying >= 0) {
const subStream = this.getStreamByIndex(streams, this._subtitleTrackIndexToSetOnPlaying);
if (subStream && subStream.DeliveryMethod === 'External' && subStream.DeliveryUrl) {
subtitleParam = '#,' + subStream.DeliveryUrl;
console.log('[MPV] External subtitle URL:', subStream.DeliveryUrl);
} else {
const relIndex = this.getRelativeIndexByType(streams, this._subtitleTrackIndexToSetOnPlaying, 'Subtitle');
subtitleParam = relIndex != null ? relIndex : -1;
console.log('[MPV] Mapped subtitle index:', this._subtitleTrackIndexToSetOnPlaying, '->', subtitleParam);
}
} else {
subtitleParam = -1;
}
console.log('[MPV] Mapped audio index:', this._audioTrackIndexToSetOnPlaying, '->', audioRelIndex);
player.load(val,
{ startMilliseconds: ms, autoplay: true },
streamdata,
this.getAudioParam(),
this.getSubtitleParam(),
audioRelIndex,
subtitleParam,
resolve);
});
}
setSubtitleStreamIndex(index) {
console.log('[MPV] setSubtitleStreamIndex called with index:', index);
this._subtitleTrackIndexToSetOnPlaying = index;
window.api.player.setSubtitleStream(this.getSubtitleParam());
if (index < 0) {
window.api.player.setSubtitleStream(-1);
return;
}
const streams = this._currentPlayOptions?.mediaSource?.MediaStreams || [];
const stream = this.getStreamByIndex(streams, index);
// Handle external subtitle URL
if (stream && stream.DeliveryMethod === 'External' && stream.DeliveryUrl) {
console.log('[MPV] Loading external subtitle:', stream.DeliveryUrl);
window.api.player.setSubtitleStream('#,' + stream.DeliveryUrl);
return;
}
// Handle embedded subtitle via relative index
const relIndex = this.getRelativeIndexByType(streams, index, 'Subtitle');
console.log('[MPV] Mapped subtitle index:', index, '->', relIndex);
window.api.player.setSubtitleStream(relIndex != null ? relIndex : -1);
}
setSecondarySubtitleStreamIndex(index) {
// MPV doesn't support secondary subtitles - no-op for compatibility
console.log('[MPV] setSecondarySubtitleStreamIndex not supported, ignoring index:', index);
}
resetSubtitleOffset() {
@@ -428,15 +425,13 @@
}
setAudioStreamIndex(index) {
console.log('[MPV] setAudioStreamIndex called with index:', index);
this._audioTrackIndexToSetOnPlaying = index;
const streams = this.getSupportedAudioStreams();
if (streams.length < 2) {
// If there's only one supported stream then trust that the player will handle it on it's own
return;
}
window.api.player.setAudioStream(this.getAudioParam());
const streams = this._currentPlayOptions?.mediaSource?.MediaStreams || [];
const relIndex = index < 0 ? -1 : this.getRelativeIndexByType(streams, index, 'Audio');
console.log('[MPV] Mapped audio index:', index, '->', relIndex);
window.api.player.setAudioStream(relIndex != null ? relIndex : -1);
}
onEndedInternal() {
@@ -464,16 +459,16 @@
}
removeMediaDialog() {
this.loading.hide();
window.api.player.stop();
window.api.power.setScreensaverEnabled(true);
this.setTransparency('none');
window.api.player.setVideoRectangle(-1, 0, 0, 0);
document.body.classList.remove('hide-scroll');
const dlg = this._videoDialog;
if (dlg) {
this.setTransparency(0); // TRANSPARENCY_LEVEL.None
this._videoDialog = null;
dlg.parentNode.removeChild(dlg);
}
@@ -505,8 +500,6 @@
const dlg = document.querySelector('.videoPlayerContainer');
if (!dlg) {
this.loading.show();
const dlg = document.createElement('div');
dlg.classList.add('videoPlayerContainer');
@@ -526,7 +519,15 @@
dlg.innerHTML = html;
// Set backdrop if available
if (options.backdropUrl) {
dlg.style.backgroundImage = `url('${options.backdropUrl}')`;
dlg.style.backgroundSize = 'cover';
dlg.style.backgroundPosition = 'center';
}
document.body.insertBefore(dlg, document.body.firstChild);
this.setTransparency(2); // TRANSPARENCY_LEVEL.Full
this._videoDialog = dlg;
const player = window.api.player;
if (!this._hasConnection) {
@@ -537,6 +538,18 @@
player.updateDuration.connect(this.onDuration);
player.error.connect(this.onError);
player.paused.connect(this.onPause);
// Log all other signals
player.buffering.connect((percent) => {
console.log(`[MPV Signal] buffering: ${percent}`);
});
player.canceled.connect(() => console.log('[MPV Signal] canceled'));
player.stopped.connect(() => console.log('[MPV Signal] stopped'));
player.stateChanged.connect((newState, oldState) => console.log(`[MPV Signal] stateChanged: ${oldState} -> ${newState}`));
player.videoPlaybackActive.connect((active) => console.log(`[MPV Signal] videoPlaybackActive: ${active}`));
player.windowVisible.connect((visible) => console.log(`[MPV Signal] windowVisible: ${visible}`));
player.onVideoRecangleChanged.connect(() => console.log('[MPV Signal] onVideoRecangleChanged'));
player.onMetaData.connect((meta, baseUrl) => console.log(`[MPV Signal] onMetaData: ${JSON.stringify(meta)}`));
}
if (options.fullscreen) {
@@ -561,6 +574,11 @@
return (mediaType || '').toLowerCase() === 'video';
}
canPlayItem(item, playOptions) {
// Delegate to canPlayMediaType - MPV can play any video the media type check passes
return this.canPlayMediaType(item.MediaType);
}
/**
* @private
*/
@@ -861,6 +879,21 @@
setAspectRatio(value) {
window.jmpInfo.settings.video.aspect = value;
}
isFullscreen() {
// Check native window fullscreen state from settings (universal source of truth)
if (window.jmpInfo && window.jmpInfo.settings && window.jmpInfo.settings.main) {
return window.jmpInfo.settings.main.fullscreen === true;
}
return false;
}
toggleFullscreen() {
// Toggle native window fullscreen via host command
if (window.api && window.api.input) {
window.api.input.executeActions(['host:fullscreen']);
}
}
}
/* eslint-enable indent */
+2 -2
View File
@@ -62,10 +62,10 @@ DAMAGE.
Others:
-------
- qt: LGPL v2.1 or v3 (http://doc.qt.io/qt-5/licensing.html)
- qt: LGPL v2.1 or v3 (http://doc.qt.io/qt-6/licensing.html)
The Qt website also lists further licenses for some of its components and
dependencies.
- qtwebengine: LGPL v3, or GPL v2 or later (http://doc.qt.io/qt-5/qtwebenginewidgets-index.html#license-information)
- qtwebengine: LGPL v3, or GPL v2 or later (http://doc.qt.io/qt-6/qtwebenginewidgets-index.html#license-information)
- mpv: GPL v2 or later (https://github.com/mpv-player/mpv/blob/master/LICENSE)
- FFmpeg: LGPL v2.1 or later (http://www.ffmpeg.org/legal.html)
- bzip2: BSD-style (http://www.bzip.org)
+1 -1
View File
@@ -26,7 +26,7 @@ def install_libraries(binary):
out = exec_cmd(["otool", "-L", binary], supress_output=True)
for line in out.split('\n'):
line = line.strip()
if line.find("dylib") != -1 and (line.startswith('@executable_path') or line.find("libQt5") != -1):
if line.find("dylib") != -1 and (line.startswith('@executable_path') or line.find("libQt6") != -1):
file = line.split('/')[-1].split(' ')[0]
if line.startswith("@executable_path"):
src = os.path.join(sys.argv[1], "dependencies/darwin-x86_64/lib", file)
+14 -5
View File
@@ -8,7 +8,7 @@ include_directories(
)
set(CMAKE_AUTOMOC ON)
set(Qt6Core_RCC_EXECUTABLE Qt6::rcc)
add_definitions(-DPREFIX="${CMAKE_INSTALL_PREFIX}")
add_subdirectory(shared)
@@ -68,8 +68,8 @@ add_custom_command(OUTPUT resources.qrc
)
set_source_files_properties(resources.qrc PROPERTIES GENERATED TRUE)
add_custom_command(OUTPUT qrc_resources.cpp
COMMAND ${Qt5Core_RCC_EXECUTABLE} -name resources -o qrc_resources.cpp resources.qrc
DEPENDS resources.qrc ${CMAKE_SOURCE_DIR}/src/ui/webview.qml ${RESOURCE_FILES}
COMMAND ${Qt6Core_RCC_EXECUTABLE} -name resources -o qrc_resources.cpp resources.qrc
DEPENDS resources.qrc ${CMAKE_SOURCE_DIR}/src/ui/webview.qml ${RESOURCE_FILES} ${NATIVE_FILES}
)
set_source_files_properties(qrc_resources.cpp PROPERTIES GENERATED TRUE)
@@ -164,7 +164,7 @@ target_link_libraries(${MAIN_TARGET}
shared
${MPV_LIBRARY}
${OPENGL_LIBS}
${QT5_LIBRARIES}
${QT6_LIBRARIES}
${OS_LIBS}
${EXTRA_LIBS}
${X11_LIBRARIES}
@@ -173,8 +173,17 @@ target_link_libraries(${MAIN_TARGET}
${CMAKE_THREAD_LIBS_INIT}
${RPI_LIBS}
${MINIZIP_LIBS}
Qt6::Core
Qt6::Quick
Qt6::Widgets
Qt6::WebEngineQuick
Qt6::WebEngineCore
Qt6::OpenGL
Qt6::Xml
Qt6::Gui
Qt6::DBus
MpvQt::MpvQt
)
install(TARGETS ${MAIN_TARGET} DESTINATION ${INSTALL_BIN_DIR})
set(EXE "${MAIN_NAME}.app")
+2
View File
@@ -12,6 +12,7 @@
#include "system/SystemComponent.h"
#include "settings/SettingsComponent.h"
#include "taskbar/TaskbarComponent.h"
#include "ui/WindowManager.h"
#if KONVERGO_OPENELEC
#include "system/openelec/OESystemComponent.h"
@@ -58,6 +59,7 @@ void ComponentManager::initialize()
registerComponent(&PlayerComponent::Get());
registerComponent(&PowerComponent::Get());
registerComponent(&TaskbarComponent::Get());
registerComponent(&WindowManager::Get());
#if KONVERGO_OPENELEC
registerComponent(&OESystemComponent::Get());
+4 -4
View File
@@ -3,9 +3,9 @@
//
#include "Globals.h"
#include "ui/KonvergoWindow.h"
#include <QQmlContext>
#include <QQuickWindow>
static QQmlApplicationEngine* g_qmlEngine = nullptr;
@@ -50,15 +50,15 @@ void Globals::EngineDestroy()
}
/////////////////////////////////////////////////////////////////////////////////////////
KonvergoWindow* Globals::MainWindow()
QQuickWindow* Globals::MainWindow()
{
Q_ASSERT_X(g_qmlEngine, "Globals", "QmlEngine not inited yet");
auto rootObject = g_qmlEngine->rootObjects().first();
Q_ASSERT_X(g_qmlEngine, "Globals", "No root objects in QmlEngine");
auto window = qobject_cast<KonvergoWindow*>(rootObject);
Q_ASSERT_X(g_qmlEngine, "Globals", "RootObject in QmlEngine is not a KonvergoWindow");
auto window = qobject_cast<QQuickWindow*>(rootObject);
Q_ASSERT_X(g_qmlEngine, "Globals", "RootObject in QmlEngine is not a QQuickWindow");
return window;
}
+2 -3
View File
@@ -2,8 +2,7 @@
#define KONVERGOENGINE_H
#include <QQmlApplicationEngine>
class KonvergoWindow;
#include <QQuickWindow>
/////////////////////////////////////////////////////////////////////////////////////////
namespace Globals
@@ -13,7 +12,7 @@ namespace Globals
void SetContextProperty(const QString& property, QObject* object);
void SetContextProperty(const QString& property, const QVariant& value);
void EngineDestroy();
KonvergoWindow* MainWindow();
QQuickWindow* MainWindow();
};
#endif // KONVERGOENGINE_H
+1
View File
@@ -9,6 +9,7 @@
#ifndef _DISPLAYMANAGER_H_
#define _DISPLAYMANAGER_H_
#include <QObject>
#include <QMap>
#include <QPoint>
#include <QString>
+1 -1
View File
@@ -176,7 +176,7 @@ void DisplayManagerRPI::resetRendering()
qInfo() << "Recreating Qt UI renderer";
// destroy the window to reset OpenGL context
window->setPersistentOpenGLContext(false);
window->setPersistentGraphics(false);
window->setPersistentSceneGraph(false);
window->destroy();
+15 -8
View File
@@ -132,13 +132,20 @@ void InputComponent::handleAction(const QString& action)
else
{
qDebug() << "Invoking slot" << qPrintable(recvSlot->m_slot.data());
QGenericArgument arg0 = QGenericArgument();
bool success;
if (recvSlot->m_hasArguments)
arg0 = Q_ARG(const QString&, hostArguments);
{
success = QMetaObject::invokeMethod(recvSlot->m_receiver, recvSlot->m_slot.data(),
Qt::AutoConnection, Q_ARG(const QString&, hostArguments));
}
else
{
success = QMetaObject::invokeMethod(recvSlot->m_receiver, recvSlot->m_slot.data(),
Qt::AutoConnection);
}
if (!QMetaObject::invokeMethod(recvSlot->m_receiver, recvSlot->m_slot.data(),
Qt::AutoConnection, arg0))
if (!success)
{
qCritical() << "Invoking slot" << qPrintable(recvSlot->m_slot.data()) << "failed!";
}
@@ -186,14 +193,14 @@ void InputComponent::remapInput(const QString &source, const QString &keycode, I
m_autoRepeatActions.clear();
auto actions = m_mappings->mapToAction(source, keycode);
for (auto action : actions)
for (const auto& action : actions)
{
if (action.type() == QVariant::String)
if (action.typeId() == QMetaType::QString)
{
queuedActions.append(action.toString());
m_autoRepeatActions.append(action.toString());
}
else if (action.type() == QVariant::Map)
else if (action.typeId() == QMetaType::QVariantMap)
{
QVariantMap map = action.toMap();
if (map.contains("long"))
@@ -211,7 +218,7 @@ void InputComponent::remapInput(const QString &source, const QString &keycode, I
queuedActions.append(map.value("short").toString());
}
}
else if (action.type() == QVariant::List)
else if (action.typeId() == QMetaType::QStringList)
{
queuedActions.append(action.toStringList());
}
+85 -9
View File
@@ -5,24 +5,31 @@
#include <QFileInfo>
#include <QIcon>
#include <QtQml>
#include <QtWebEngine/qtwebengineglobal.h>
#include <QtWebEngineWidgets/QWebEngineProfile>
#include <Qt>
#include <QtWebEngineQuick>
#include <qtwebenginecoreglobal.h>
#include <QtWebEngineCore/QWebEngineProfile>
#include <QErrorMessage>
#include <QtWebEngineCore/QWebEngineScript>
#include <QCommandLineOption>
#include <QDebug>
#include <QSettings>
#include "shared/Names.h"
#include "system/SystemComponent.h"
#include <QDebug>
#include "Paths.h"
#include "player/CodecsComponent.h"
#include "player/PlayerComponent.h"
#include "player/OpenGLDetect.h"
#include "display/DisplayComponent.h"
#include "Version.h"
#include "settings/SettingsComponent.h"
#include "settings/SettingsSection.h"
#include "ui/KonvergoWindow.h"
#include "ui/KonvergoWindow.h"
#include "ui/EventFilter.h"
#include "ui/WindowManager.h"
#include "Globals.h"
#include "ui/ErrorMessage.h"
#include "UniqueApplication.h"
@@ -76,9 +83,13 @@ char** appendCommandLineArguments(int argc, char **argv, const QStringList& args
void ShowLicenseInfo()
{
QFile licenses(":/misc/licenses.txt");
licenses.open(QIODevice::ReadOnly | QIODevice::Text);
if (!licenses.open(QIODevice::ReadOnly | QIODevice::Text))
{
fprintf(stderr, "Error: Could not open license file\n");
return;
}
QByteArray contents = licenses.readAll();
printf("%.*s\n", contents.size(), contents.data());
printf("%.*s\n", static_cast<int>(contents.size()), contents.data());
}
/////////////////////////////////////////////////////////////////////////////////////////
@@ -139,6 +150,8 @@ int main(int argc, char *argv[])
preinitQt();
detectOpenGLEarly();
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
QStringList arguments;
for (int i = 0; i < argc; i++)
@@ -178,9 +191,15 @@ int main(int argc, char *argv[])
auto scale = parser.value("scale-factor");
if (scale.isEmpty() || scale == "auto")
{
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
}
else if (scale != "none")
{
qputenv("QT_SCALE_FACTOR", scale.toUtf8());
}
auto platform = parser.value("platform");
if (!(platform.isEmpty() || platform == "default"))
@@ -214,6 +233,7 @@ int main(int argc, char *argv[])
webEngineDataDir = d.absolutePath() + "/QtWebEngine";
}
QtWebEngineQuick::initialize();
QApplication app(newArgc, newArgv);
#if defined(Q_OS_WIN)
@@ -262,8 +282,6 @@ int main(int argc, char *argv[])
SettingsComponent::Get().setCommandLineValues(parser.optionNames());
QtWebEngine::initialize();
// Configure QtWebEngine paths
QWebEngineProfile* defaultProfile = QWebEngineProfile::defaultProfile();
defaultProfile->setCachePath(webEngineDataDir);
@@ -279,20 +297,78 @@ int main(int argc, char *argv[])
// if we get a valid object passed to it. Any error messages will be reported on stderr
// but since no normal user should ever see this it should be fine
//
QObject::connect(engine, &QQmlApplicationEngine::objectCreated, [=](QObject* object, const QUrl& url)
QObject::connect(engine, &QQmlApplicationEngine::objectCreated, [&](QObject* object, const QUrl& url)
{
Q_UNUSED(url);
if (object == nullptr)
throw FatalException(QObject::tr("Failed to parse application engine script."));
KonvergoWindow* window = Globals::MainWindow();
QQuickWindow* window = Globals::MainWindow();
// Set window flags for proper popup handling (e.g., WebEngineView dropdowns)
window->setFlags(window->flags() | Qt::WindowFullscreenButtonHint);
// Install event filter for proper event handling
window->installEventFilter(new EventFilter(window));
// Install application event filter to catch popup window creation early
class PopupFixer : public QObject {
QQuickWindow* m_mainWindow;
public:
PopupFixer(QQuickWindow* mainWin) : m_mainWindow(mainWin) {}
bool eventFilter(QObject* obj, QEvent* event) override {
QWindow* win = qobject_cast<QWindow*>(obj);
if (!win || win == m_mainWindow) {
return QObject::eventFilter(obj, event);
}
// Fix WebEngineView popup flags to accept focus
if (event->type() == QEvent::Show) {
Qt::WindowFlags flags = win->flags();
// Only fix WebEngineView dropdowns (Tool + FramelessWindowHint + WindowStaysOnTopHint)
// Don't touch other windows (e.g., MPV-related)
bool isWebEnginePopup = (flags & Qt::Tool) &&
(flags & Qt::FramelessWindowHint) &&
(flags & Qt::WindowStaysOnTopHint);
if (!isWebEnginePopup) {
return QObject::eventFilter(obj, event);
}
if (win->transientParent() == nullptr) {
win->setTransientParent(m_mainWindow);
}
if (win->modality() != Qt::NonModal) {
win->setModality(Qt::NonModal);
}
// WebEngineView creates popups with Qt::Tool | WindowDoesNotAcceptFocus
// which prevents interaction. Change to Qt::Popup to accept focus.
flags &= ~Qt::Tool;
flags |= Qt::Popup;
flags &= ~Qt::WindowDoesNotAcceptFocus;
win->setFlags(flags);
}
return QObject::eventFilter(obj, event);
}
};
app.installEventFilter(new PopupFixer(window));
QObject* webChannel = qvariant_cast<QObject*>(window->property("webChannel"));
Q_ASSERT(webChannel);
ComponentManager::Get().setWebChannel(qobject_cast<QWebChannel*>(webChannel));
QObject::connect(uniqueApp, &UniqueApplication::otherApplicationStarted, window, &KonvergoWindow::otherAppFocus);
// Initialize WindowManager with window reference
WindowManager::Get().initializeWindow(window);
// Handle other app focus by raising window
QObject::connect(uniqueApp, &UniqueApplication::otherApplicationStarted, []() {
WindowManager::Get().raiseWindow();
});
});
engine->load(QUrl(QStringLiteral("qrc:/ui/webview.qml")));
+1 -1
View File
@@ -1,5 +1,5 @@
add_sources(PlayerComponent.cpp PlayerComponent.h)
add_sources(PlayerQuickItem.cpp PlayerQuickItem.h)
add_sources(MpvVideoItem.cpp MpvVideoItem.h)
add_sources(CodecsComponent.cpp CodecsComponent.h)
add_sources(OpenGLDetect.cpp OpenGLDetect.h)
add_sources(QtHelper.h)
+39
View File
@@ -0,0 +1,39 @@
#include "MpvVideoItem.h"
#include "PlayerComponent.h"
#include <MpvController>
#include <QDebug>
MpvVideoItem::MpvVideoItem(QQuickItem *parent)
: MpvAbstractItem(parent)
{
qDebug() << "MpvVideoItem constructed";
// Critical: Set vo=libmpv for Qt integration
Q_EMIT setProperty("vo", "libmpv");
}
void MpvVideoItem::setPlayerComponent(PlayerComponent* player)
{
qDebug() << "MpvVideoItem::setPlayerComponent called, mpvController():" << mpvController();
m_player = player;
// When mpv is ready, give controller to PlayerComponent
connect(this, &MpvAbstractItem::ready, this, [this]() {
qDebug() << "MpvVideoItem ready() signal fired!";
if (m_player && mpvController()) {
qDebug() << "Setting mpv controller and initializing";
m_player->setMpvController(mpvController());
m_player->initializeMpv();
} else {
qWarning() << "ready() fired but m_player:" << m_player << "mpvController():" << mpvController();
}
});
// Check if already ready
if (mpvController()) {
qDebug() << "MpvVideoItem already ready, initializing now";
m_player->setMpvController(mpvController());
m_player->initializeMpv();
} else {
qDebug() << "MpvVideoItem not ready yet, waiting for ready() signal";
}
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef MPVVIDEOITEM_H
#define MPVVIDEOITEM_H
#include <MpvAbstractItem>
class PlayerComponent;
class MpvVideoItem : public MpvAbstractItem
{
Q_OBJECT
QML_ELEMENT
public:
explicit MpvVideoItem(QQuickItem *parent = nullptr);
void setPlayerComponent(PlayerComponent* player);
MpvController* controller() { return mpvController(); }
private:
PlayerComponent* m_player = nullptr;
};
#endif // MPVVIDEOITEM_H
+1
View File
@@ -41,6 +41,7 @@ static QString probeHwdecInterop()
auto mpv = mpv::qt::Handle::FromRawHandle(mpv_create());
if (!mpv)
return "";
mpv::qt::set_property(mpv, "gpu-hwdec-interop", "auto");
// Actually creating a window is required. There is currently no way to keep
// this window hidden or invisible.
mpv::qt::set_property(mpv, "force-window", true);
+247 -142
View File
@@ -13,12 +13,14 @@
#include "ComponentManager.h"
#include "settings/SettingsSection.h"
#include "PlayerQuickItem.h"
#include "MpvVideoItem.h"
#include "input/InputComponent.h"
#include <MpvController>
#include <math.h>
#include <string.h>
#include <shared/Paths.h>
#include <QRegularExpression>
#if !defined(Q_OS_WIN)
#include <unistd.h>
@@ -46,10 +48,11 @@ PlayerComponent::PlayerComponent(QObject* parent)
m_window(nullptr), m_mediaFrameRate(0),
m_restoreDisplayTimer(this), m_reloadAudioTimer(this),
m_streamSwitchImminent(false), m_doAc3Transcoding(false),
m_videoRectangle(-1, -1, -1, -1)
m_videoRectangle(-1, 0, 0, 0)
{
qmlRegisterType<PlayerQuickItem>("Konvergo", 1, 0, "MpvVideo"); // deprecated name
qmlRegisterType<PlayerQuickItem>("Konvergo", 1, 0, "KonvergoVideo");
qmlRegisterType<MpvVideoItem>("Konvergo", 1, 0, "MpvVideo"); // deprecated name
qmlRegisterType<MpvVideoItem>("Konvergo", 1, 0, "KonvergoVideo");
qmlRegisterType<MpvVideoItem>("Konvergo", 1, 0, "MpvVideoItem");
m_restoreDisplayTimer.setSingleShot(true);
connect(&m_restoreDisplayTimer, &QTimer::timeout, this, &PlayerComponent::onRestoreDisplay);
@@ -69,110 +72,118 @@ void PlayerComponent::componentPostInitialize()
///////////////////////////////////////////////////////////////////////////////////////////////////
PlayerComponent::~PlayerComponent()
{
if (m_mpv)
mpv_set_wakeup_callback(m_mpv, nullptr, nullptr);
// m_mpv is owned by MpvVideoItem, don't access it here as it may be destroyed
}
///////////////////////////////////////////////////////////////////////////////////////////////////
bool PlayerComponent::componentInitialize()
{
m_mpv = mpv::qt::Handle::FromRawHandle(mpv_create());
// Defer mpv creation until setQtQuickWindow() where we get MpvQt's handle
// m_mpv will be set via setMpvHandle() called from MpvVideoItem::initMpv()
return true;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::initializeMpv()
{
if (!m_mpv)
throw FatalException(tr("Failed to load mpv."));
mpv_request_log_messages(m_mpv, "terminal-default");
mpv::qt::set_property(m_mpv, "msg-level", "all=v");
// MpvQt already called mpv_initialize(), so mpv is ready
// Properties that needed to be set before init were set in MpvVideoItem constructor
// Configuration properties defined in the mpv.conf will override our
// hardcoded properties below.
mpv::qt::set_property(m_mpv, "config", "yes");
mpv::qt::set_property(m_mpv, "config-dir", Paths::dataDir());
mpv_request_log_messages(m_mpv->mpv(), "terminal-default");
m_mpv->setProperty("msg-level", "all=v");
mpv_set_wakeup_callback(m_mpv, wakeup_cb, this);
mpv_set_wakeup_callback(m_mpv->mpv(), wakeup_cb, this);
// Keep window open even when idle (no file loaded)
m_mpv->setProperty("force-window", true);
// Disable native OSD if mpv_command_string() is used.
mpv::qt::set_property(m_mpv, "osd-level", "0");
m_mpv->setProperty("osd-level", "0");
// This forces the player not to rebase playback time to 0 with mkv. We
// require this, because mkv transcoding lets files start at times other
// than 0, and web-client expects that we return these times unchanged.
mpv::qt::set_property(m_mpv, "demuxer-mkv-probe-start-time", false);
m_mpv->setProperty( "demuxer-mkv-probe-start-time", false);
// Upstream mpv sets this to "auto", which disables probing for HLS (at least),
// in order to speed up playback start. The situation is more complex in PMP
// due to us wanting to use system codecs, so always enable this.
mpv::qt::set_property(m_mpv, "demuxer-lavf-probe-info", true);
m_mpv->setProperty( "demuxer-lavf-probe-info", true);
// Just discard audio output if no audio device could be opened. This gives
// us better flexibility how to react to such errors (instead of just
// aborting playback immediately).
mpv::qt::set_property(m_mpv, "audio-fallback-to-null", "yes");
m_mpv->setProperty( "audio-fallback-to-null", "yes");
// Do not let the decoder downmix (better customization for us).
mpv::qt::set_property(m_mpv, "ad-lavc-downmix", false);
m_mpv->setProperty( "ad-lavc-downmix", false);
// User-visible application name used by some audio APIs (at least PulseAudio).
mpv::qt::set_property(m_mpv, "audio-client-name", QCoreApplication::applicationName());
m_mpv->setProperty( "audio-client-name", QCoreApplication::applicationName());
// User-visible stream title used by some audio APIs (at least PulseAudio and wasapi).
mpv::qt::set_property(m_mpv, "title", QCoreApplication::applicationName());
m_mpv->setProperty( "title", QCoreApplication::applicationName());
// See: https://github.com/plexinc/plex-media-player/issues/736
mpv::qt::set_property(m_mpv, "cache-seek-min", 5000);
m_mpv->setProperty( "cache-seek-min", 5000);
// Disable ytdl
mpv::qt::set_property(m_mpv, "ytdl", false);
m_mpv->setProperty( "ytdl", false);
if (SettingsComponent::Get().ignoreSSLErrors()) {
mpv::qt::set_property(m_mpv, "tls-ca-file", "");
mpv::qt::set_property(m_mpv, "tls-verify", "no");
m_mpv->setProperty( "tls-ca-file", "");
m_mpv->setProperty( "tls-verify", "no");
} else {
#if !defined(Q_OS_WIN) && !defined(Q_OS_MAC)
if (SettingsComponent::Get().autodetectCertBundle()) {
QString certPath = SettingsComponent::Get().detectCertBundlePath();
if (!certPath.isEmpty()) {
mpv::qt::set_property(m_mpv, "tls-ca-file", certPath);
mpv::qt::set_property(m_mpv, "tls-verify", QString("yes"));
m_mpv->setProperty("tls-ca-file", certPath);
m_mpv->setProperty("tls-verify", QString("yes"));
} else {
throw FatalException(tr("Failed to locate CA bundle."));
}
} else {
mpv::qt::set_property(m_mpv, "tls-verify", "yes");
m_mpv->setProperty( "tls-verify", "yes");
}
#else
// We need to not use Shinchiro's personal CA file...
mpv::qt::set_property(m_mpv, "tls-ca-file", "");
m_mpv->setProperty( "tls-ca-file", "");
#endif
}
// Apply some low-memory settings on RPI, which is relatively memory-constrained.
#ifdef TARGET_RPI
// The backbuffer makes seeking back faster (without having to do a HTTP-level seek)
mpv::qt::set_property(m_mpv, "cache-backbuffer", 10 * 1024); // KB
m_mpv->setProperty( "cache-backbuffer", 10 * 1024); // KB
// The demuxer queue is used for the readahead, and also for dealing with badly
// interlaved audio/video. Setting it too low increases sensitivity to network
// issues, and could cause playback failure with "bad" files.
mpv::qt::set_property(m_mpv, "demuxer-max-bytes", 50 * 1024 * 1024); // bytes
m_mpv->setProperty( "demuxer-max-bytes", 50 * 1024 * 1024); // bytes
// Specifically for enabling mpeg4.
mpv::qt::set_property(m_mpv, "hwdec-codecs", "all");
m_mpv->setProperty( "hwdec-codecs", "all");
// Do not use exact seeks by default. (This affects the start position in the "loadfile"
// command in particular. We override the seek mode for normal "seek" commands.)
mpv::qt::set_property(m_mpv, "hr-seek", "no");
m_mpv->setProperty( "hr-seek", "no");
// Force vo_rpi to fullscreen.
mpv::qt::set_property(m_mpv, "fullscreen", true);
m_mpv->setProperty( "fullscreen", true);
#endif
if (mpv_initialize(m_mpv) < 0)
throw FatalException(tr("Failed to initialize mpv."));
// MpvQt already called mpv_initialize() - don't call it again
// if (mpv_initialize(m_mpv) < 0)
// throw FatalException(tr("Failed to initialize mpv."));
mpv_observe_property(m_mpv, 0, "pause", MPV_FORMAT_FLAG);
mpv_observe_property(m_mpv, 0, "core-idle", MPV_FORMAT_FLAG);
mpv_observe_property(m_mpv, 0, "cache-buffering-state", MPV_FORMAT_INT64);
mpv_observe_property(m_mpv, 0, "playback-time", MPV_FORMAT_DOUBLE);
mpv_observe_property(m_mpv, 0, "vo-configured", MPV_FORMAT_FLAG);
mpv_observe_property(m_mpv, 0, "duration", MPV_FORMAT_DOUBLE);
mpv_observe_property(m_mpv, 0, "audio-device-list", MPV_FORMAT_NODE);
mpv_observe_property(m_mpv, 0, "video-dec-params", MPV_FORMAT_NODE);
mpv_observe_property(m_mpv->mpv(), 0, "pause", MPV_FORMAT_FLAG);
mpv_observe_property(m_mpv->mpv(), 0, "core-idle", MPV_FORMAT_FLAG);
mpv_observe_property(m_mpv->mpv(), 0, "cache-buffering-state", MPV_FORMAT_INT64);
mpv_observe_property(m_mpv->mpv(), 0, "playback-time", MPV_FORMAT_DOUBLE);
mpv_observe_property(m_mpv->mpv(), 0, "vo-configured", MPV_FORMAT_FLAG);
mpv_observe_property(m_mpv->mpv(), 0, "duration", MPV_FORMAT_DOUBLE);
mpv_observe_property(m_mpv->mpv(), 0, "audio-device-list", MPV_FORMAT_NODE);
mpv_observe_property(m_mpv->mpv(), 0, "video-dec-params", MPV_FORMAT_NODE);
// Setup a hook with the ID 1, which is run during the file is loaded.
// Used to delay playback start for display framerate switching.
@@ -180,11 +191,11 @@ bool PlayerComponent::componentInitialize()
// Setup a hook with the ID 2, which is run at a certain stage during loading.
// We use it to initialize stream selections and to probe the codecs.
#if MPV_CLIENT_API_VERSION < MPV_MAKE_VERSION(1, 100)
mpv::qt::command(m_mpv, QStringList() << "hook-add" << "on_load" << "1" << "0");
mpv::qt::command(m_mpv, QStringList() << "hook-add" << "on_preloaded" << "2" << "0");
m_mpv->command( QStringList() << "hook-add" << "on_load" << "1" << "0");
m_mpv->command( QStringList() << "hook-add" << "on_preloaded" << "2" << "0");
#else
mpv_hook_add(m_mpv, 1, "on_load", 0);
mpv_hook_add(m_mpv, 2, "on_preloaded", 0);
mpv_hook_add(m_mpv->mpv(), 1, "on_load", 0);
mpv_hook_add(m_mpv->mpv(), 2, "on_preloaded", 0);
#endif
updateAudioDeviceList();
@@ -224,8 +235,6 @@ bool PlayerComponent::componentInitialize()
connect(this, &PlayerComponent::onMpvEvents, this, &PlayerComponent::handleMpvEvents, Qt::QueuedConnection);
emit onMpvEvents();
return true;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -242,11 +251,15 @@ void PlayerComponent::setVideoRectangle(int x, int y, int w, int h)
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::setQtQuickWindow(QQuickWindow* window)
{
PlayerQuickItem* video = window->findChild<PlayerQuickItem*>("video");
if (!video)
qDebug() << "PlayerComponent::setQtQuickWindow called";
MpvVideoItem* video = window->findChild<MpvVideoItem*>("video");
if (!video) {
qCritical() << "Failed to find MpvVideoItem with objectName 'video'";
throw FatalException(tr("Failed to load video element."));
}
video->initMpv(this);
qDebug() << "Found MpvVideoItem, calling setPlayerComponent";
video->setPlayerComponent(this);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -267,14 +280,15 @@ void PlayerComponent::setWindow(QQuickWindow* window)
if (forceVo.size())
vo = forceVo;
mpv::qt::set_property(m_mpv, "vo", vo);
// MpvQt sets vo=libmpv in MpvVideoItem constructor
// Don't set it here since m_mpv may be null (MpvQt not ready yet)
if (vo == "libmpv")
setQtQuickWindow(window);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
bool PlayerComponent::load(const QString& url, const QVariantMap& options, const QVariantMap &metadata, const QString& audioStream , const QString& subtitleStream)
bool PlayerComponent::load(const QString& url, const QVariantMap& options, const QVariantMap &metadata, const QVariant& audioStream , const QVariant& subtitleStream)
{
stop();
queueMedia(url, options, metadata, audioStream, subtitleStream);
@@ -282,8 +296,13 @@ bool PlayerComponent::load(const QString& url, const QVariantMap& options, const
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::queueMedia(const QString& url, const QVariantMap& options, const QVariantMap &metadata, const QString& audioStream, const QString& subtitleStream)
void PlayerComponent::queueMedia(const QString& url, const QVariantMap& options, const QVariantMap &metadata, const QVariant& audioStream, const QVariant& subtitleStream)
{
if (!m_mpv) {
qWarning() << "PlayerComponent::queueMedia: mpv not initialized yet";
return;
}
InputComponent::Get().cancelAutoRepeat();
m_mediaFrameRate = metadata["frameRate"].toFloat(); // returns 0 on failure
@@ -330,7 +349,7 @@ void PlayerComponent::queueMedia(const QString& url, const QVariantMap& options,
command << extraArgs;
mpv::qt::command(m_mpv, command);
m_mpv->command( command);
emit onMetaData(metadata["metadata"].toMap(), qurl.adjusted(QUrl::RemovePath | QUrl::RemoveQuery));
}
@@ -388,7 +407,7 @@ bool PlayerComponent::switchDisplayFrameRate()
void PlayerComponent::onRestoreDisplay()
{
// If the player will in fact start another file (or is playing one), don't restore.
if (mpv::qt::get_property(m_mpv, "idle-active").toBool())
if (m_mpv->getProperty( "idle-active").toBool())
DisplayComponent::Get().restorePreviousVideoMode();
}
@@ -601,7 +620,7 @@ void PlayerComponent::handleMpvEvent(mpv_event *event)
qInfo() << "checking codecs";
startCodecsLoading([=] {
qInfo() << "resuming loading";
mpv::qt::command(m_mpv, QStringList() << "hook-ack" << resumeId);
m_mpv->command( QStringList() << "hook-ack" << resumeId);
});
};
if (switchDisplayFrameRate())
@@ -627,7 +646,7 @@ void PlayerComponent::handleMpvEvent(mpv_event *event)
reselectStream(m_currentSubtitleStream, MediaType::Subtitle);
reselectStream(m_currentAudioStream, MediaType::Audio);
startCodecsLoading([=] {
mpv::qt::command(m_mpv, QStringList() << "hook-ack" << resumeId);
m_mpv->command( QStringList() << "hook-ack" << resumeId);
});
break;
}
@@ -646,7 +665,7 @@ void PlayerComponent::handleMpvEvent(mpv_event *event)
qInfo() << "checking codecs";
startCodecsLoading([=] {
qInfo() << "resuming loading";
mpv_hook_continue(m_mpv, id);
mpv_hook_continue(m_mpv->mpv(), id);
});
};
if (switchDisplayFrameRate())
@@ -670,7 +689,7 @@ void PlayerComponent::handleMpvEvent(mpv_event *event)
reselectStream(m_currentSubtitleStream, MediaType::Subtitle);
reselectStream(m_currentAudioStream, MediaType::Audio);
startCodecsLoading([=] {
mpv_hook_continue(m_mpv, id);
mpv_hook_continue(m_mpv->mpv(), id);
});
break;
}
@@ -688,7 +707,7 @@ void PlayerComponent::handleMpvEvents()
// Process all events, until the event queue is empty.
while (1)
{
mpv_event *event = mpv_wait_event(m_mpv, 0);
mpv_event *event = mpv_wait_event(m_mpv->mpv(), 0);
if (event->event_id == MPV_EVENT_NONE)
break;
handleMpvEvent(event);
@@ -711,62 +730,98 @@ void PlayerComponent::setVideoOnlyMode(bool enable)
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::play()
{
if (!m_mpv) {
qWarning() << "PlayerComponent::play: mpv not initialized yet";
return;
}
QStringList args = (QStringList() << "set" << "pause" << "no");
mpv::qt::command(m_mpv, args);
m_mpv->command( args);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::stop()
{
if (!m_mpv) {
qWarning() << "PlayerComponent::stop: mpv not initialized yet";
return;
}
QStringList args("stop");
mpv::qt::command(m_mpv, args);
m_mpv->command( args);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::clearQueue()
{
if (!m_mpv) {
qWarning() << "PlayerComponent::clearQueue: mpv not initialized yet";
return;
}
QStringList args("playlist_clear");
mpv::qt::command(m_mpv, args);
m_mpv->command( args);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::pause()
{
if (!m_mpv) {
qWarning() << "PlayerComponent::pause: mpv not initialized yet";
return;
}
QStringList args = (QStringList() << "set" << "pause" << "yes");
mpv::qt::command(m_mpv, args);
m_mpv->command( args);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::seekTo(qint64 ms)
{
if (!m_mpv) {
qWarning() << "PlayerComponent::seekTo: mpv not initialized yet";
return;
}
double timeSecs = ms / 1000.0;
QVariantList args = (QVariantList() << "seek" << timeSecs << "absolute+exact");
mpv::qt::command(m_mpv, args);
m_mpv->command( args);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
QVariant PlayerComponent::getAudioDeviceList()
{
return mpv::qt::get_property(m_mpv, "audio-device-list");
if (!m_mpv) {
qWarning() << "PlayerComponent::getAudioDeviceList: mpv not initialized yet";
return QVariant();
}
return m_mpv->getProperty( "audio-device-list");
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::setAudioDevice(const QString& name)
{
mpv::qt::set_property(m_mpv, "audio-device", name);
if (!m_mpv) {
qWarning() << "PlayerComponent::setAudioDevice: mpv not initialized yet";
return;
}
m_mpv->setProperty( "audio-device", name);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::setVolume(int volume)
{
if (!m_mpv) {
qWarning() << "PlayerComponent::setVolume: mpv not initialized yet";
return;
}
// Will fail if no audio output opened (i.e. no file playing)
mpv::qt::set_property(m_mpv, "volume", volume);
m_mpv->setProperty( "volume", volume);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
int PlayerComponent::volume()
{
QVariant volume = mpv::qt::get_property(m_mpv, "volume");
if (!m_mpv) {
qWarning() << "PlayerComponent::volume: mpv not initialized yet";
return 0;
}
QVariant volume = m_mpv->getProperty( "volume");
if (volume.isValid())
return volume.toInt();
return 0;
@@ -775,14 +830,22 @@ int PlayerComponent::volume()
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::setMuted(bool muted)
{
if (!m_mpv) {
qWarning() << "PlayerComponent::setMuted: mpv not initialized yet";
return;
}
// Will fail if no audio output opened (i.e. no file playing)
mpv::qt::set_property(m_mpv, "mute", muted);
m_mpv->setProperty( "mute", muted);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
bool PlayerComponent::muted()
{
QVariant mute = mpv::qt::get_property(m_mpv, "mute");
if (!m_mpv) {
qWarning() << "PlayerComponent::muted: mpv not initialized yet";
return false;
}
QVariant mute = m_mpv->getProperty( "mute");
if (mute.isValid())
return mute.toBool();
return false;
@@ -791,10 +854,14 @@ bool PlayerComponent::muted()
///////////////////////////////////////////////////////////////////////////////////////////////////
QVariantList PlayerComponent::findStreamsForURL(const QString &url)
{
if (!m_mpv) {
qWarning() << "PlayerComponent::findStreamsForURL: mpv not initialized yet";
return QVariantList();
}
bool isExternal = !url.isEmpty();
QVariantList res;
auto tracks = mpv::qt::get_property(m_mpv, "track-list");
auto tracks = m_mpv->getProperty( "track-list");
for (auto track : tracks.toList())
{
QVariantMap map = track.toMap();
@@ -810,8 +877,13 @@ QVariantList PlayerComponent::findStreamsForURL(const QString &url)
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::reselectStream(const QString &streamSelection, MediaType target)
void PlayerComponent::reselectStream(const QVariant &streamSelection, MediaType target)
{
if (!m_mpv) {
qWarning() << "PlayerComponent::reselectStream: mpv not initialized yet";
return;
}
QString streamIdPropertyName;
QString streamAddCommandName;
QString mpvStreamTypeName;
@@ -830,28 +902,45 @@ void PlayerComponent::reselectStream(const QString &streamSelection, MediaType t
break;
}
// Check for string format first (handles "#,URL" and "#index")
QString streamSelectionStr = streamSelection.toString();
bool isStringFormat = streamSelectionStr.startsWith("#") || !streamSelection.canConvert<int>();
// Handle integer Jellyfin stream index (new format)
if (!isStringFormat && streamSelection.canConvert<int>()) {
int index = streamSelection.toInt();
if (index < 0) {
m_mpv->setProperty(streamIdPropertyName, "no");
} else {
// Jellyfin stream index is the MPV track ID (already 1-based)
m_mpv->setProperty(streamIdPropertyName, index);
}
return;
}
// Handle legacy string format "#1" or "#,http://..."
QString streamName;
QString streamID;
if (streamSelection.startsWith("#"))
if (streamSelectionStr.startsWith("#"))
{
int splitPos = streamSelection.indexOf(",");
int splitPos = streamSelectionStr.indexOf(",");
if (splitPos < 0)
{
// Stream from the main file
streamID = streamSelection.mid(1);
streamID = streamSelectionStr.mid(1);
streamName = "";
}
else
{
// Stream from an external file
streamID = streamSelection.mid(1, splitPos - 1);
streamName = streamSelection.mid(splitPos + 1);
streamID = streamSelectionStr.mid(1, splitPos - 1);
streamName = streamSelectionStr.mid(splitPos + 1);
}
}
else if (streamSelection.isEmpty())
else if (streamSelectionStr.isEmpty() || !streamSelection.isValid())
{
mpv::qt::set_property(m_mpv, streamIdPropertyName, "no");
m_mpv->setProperty( streamIdPropertyName, "no");
return;
}
@@ -861,7 +950,7 @@ void PlayerComponent::reselectStream(const QString &streamSelection, MediaType t
if (streams.isEmpty())
{
QStringList args = (QStringList() << streamAddCommandName << streamName);
mpv::qt::command(m_mpv, args);
m_mpv->command( args);
}
}
@@ -890,18 +979,18 @@ void PlayerComponent::reselectStream(const QString &streamSelection, MediaType t
if ((target == MediaType::Audio || !streamID.isEmpty()) && selection == "no")
selection = "1";
mpv::qt::set_property(m_mpv, streamIdPropertyName, selection);
m_mpv->setProperty( streamIdPropertyName, selection);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::setSubtitleStream(const QString &subtitleStream)
void PlayerComponent::setSubtitleStream(const QVariant &subtitleStream)
{
m_currentSubtitleStream = subtitleStream;
reselectStream(m_currentSubtitleStream, MediaType::Subtitle);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::setAudioStream(const QString &audioStream)
void PlayerComponent::setAudioStream(const QVariant &audioStream)
{
m_currentAudioStream = audioStream;
reselectStream(m_currentAudioStream, MediaType::Audio);
@@ -922,26 +1011,38 @@ void PlayerComponent::setAudioDelay(qint64 milliseconds)
audioDelaySetting = "audio_delay.50hz";
double fixedDelay = SettingsComponent::Get().value(SETTINGS_SECTION_VIDEO, audioDelaySetting).toFloat();
mpv::qt::set_property(m_mpv, "audio-delay", (fixedDelay + m_playbackAudioDelay) / 1000.0);
m_mpv->setProperty( "audio-delay", (fixedDelay + m_playbackAudioDelay) / 1000.0);
}
/////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::setSubtitleDelay(qint64 milliseconds)
{
mpv::qt::set_property(m_mpv, "sub-delay", milliseconds / 1000.0);
if (!m_mpv) {
qWarning() << "PlayerComponent::setSubtitleDelay: mpv not initialized yet";
return;
}
m_mpv->setProperty( "sub-delay", milliseconds / 1000.0);
}
/////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::setPlaybackRate(int rate)
{
mpv::qt::set_property(m_mpv, "speed", rate / 1000.0);
if (!m_mpv) {
qWarning() << "PlayerComponent::setPlaybackRate: mpv not initialized yet";
return;
}
m_mpv->setProperty( "speed", rate / 1000.0);
}
/////////////////////////////////////////////////////////////////////////////////////////
qint64 PlayerComponent::getPosition()
{
QVariant time = mpv::qt::get_property(m_mpv, "playback-time");
if (time.canConvert(QMetaType::Double))
if (!m_mpv) {
qWarning() << "PlayerComponent::getPosition: mpv not initialized yet";
return 0;
}
QVariant time = m_mpv->getProperty( "playback-time");
if (time.canConvert<double>())
return time.toDouble();
return 0;
}
@@ -949,8 +1050,12 @@ qint64 PlayerComponent::getPosition()
/////////////////////////////////////////////////////////////////////////////////////////
qint64 PlayerComponent::getDuration()
{
QVariant time = mpv::qt::get_property(m_mpv, "duration");
if (time.canConvert(QMetaType::Double))
if (!m_mpv) {
qWarning() << "PlayerComponent::getDuration: mpv not initialized yet";
return 0;
}
QVariant time = m_mpv->getProperty( "duration");
if (time.canConvert<double>())
return time.toDouble();
return 0;
}
@@ -994,7 +1099,7 @@ void PlayerComponent::updateAudioDeviceList()
QSet<QString> devices;
for(const QVariant& d : list.toList())
{
Q_ASSERT(d.type() == QVariant::Map);
Q_ASSERT(d.typeId() == QMetaType::QVariantMap);
QVariantMap dmap = d.toMap();
QString device = dmap["name"].toString();
@@ -1038,7 +1143,7 @@ void PlayerComponent::updateAudioDevice()
device = "auto";
}
mpv::qt::set_property(m_mpv, "audio-device", device);
m_mpv->setProperty( "audio-device", device);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -1053,15 +1158,15 @@ void PlayerComponent::setAudioConfiguration()
{
QString deviceType = SettingsComponent::Get().value(SETTINGS_SECTION_AUDIO, "devicetype").toString();
mpv::qt::set_property(m_mpv, "audio-exclusive", SettingsComponent::Get().value(SETTINGS_SECTION_AUDIO, "exclusive").toBool());
m_mpv->setProperty( "audio-exclusive", SettingsComponent::Get().value(SETTINGS_SECTION_AUDIO, "exclusive").toBool());
updateAudioDevice();
bool normalize = SettingsComponent::Get().value(SETTINGS_SECTION_AUDIO, "normalize").toBool();
mpv::qt::set_property(m_mpv, "audio-normalize-downmix", normalize ? "yes" : "no");
m_mpv->setProperty( "audio-normalize-downmix", normalize ? "yes" : "no");
// Make downmix more similar to PHT.
mpv::qt::set_property(m_mpv, "audio-swresample-o", "surround_mix_level=1");
m_mpv->setProperty( "audio-swresample-o", "surround_mix_level=1");
m_passthroughCodecs.clear();
@@ -1088,7 +1193,7 @@ void PlayerComponent::setAudioConfiguration()
}
QString passthroughCodecs = m_passthroughCodecs.join(",");
mpv::qt::set_property(m_mpv, "audio-spdif", passthroughCodecs);
m_mpv->setProperty( "audio-spdif", passthroughCodecs);
// set the channel layout
QVariant layout = SettingsComponent::Get().value(SETTINGS_SECTION_AUDIO, "channels");
@@ -1097,7 +1202,7 @@ void PlayerComponent::setAudioConfiguration()
if (deviceType == AUDIO_DEVICE_TYPE_SPDIF)
layout = "2.0";
mpv::qt::set_property(m_mpv, "audio-channels", layout);
m_mpv->setProperty( "audio-channels", layout);
// if the user has indicated that PCM only works for stereo, and that
// the receiver supports AC3, set this extra option that allows us to transcode
@@ -1105,17 +1210,17 @@ void PlayerComponent::setAudioConfiguration()
// here for now. We might need to add support for DTS transcoding
// if we see user requests for it.
//
m_doAc3Transcoding = false;
if (deviceType == AUDIO_DEVICE_TYPE_SPDIF &&
SettingsComponent::Get().value(SETTINGS_SECTION_AUDIO, "passthrough.ac3").toBool())
m_doAc3Transcoding =
(deviceType == AUDIO_DEVICE_TYPE_SPDIF &&
SettingsComponent::Get().value(SETTINGS_SECTION_AUDIO, "passthrough.ac3").toBool());
if (m_doAc3Transcoding)
{
QString filterArgs = "";
mpv::qt::command(m_mpv, QStringList() << "af" << "add" << ("@ac3:lavcac3enc" + filterArgs));
m_doAc3Transcoding = true;
m_mpv->command( QStringList() << "af" << "add" << ("lavcac3enc" + filterArgs));
}
else
{
mpv::qt::command(m_mpv, QStringList() << "af" << "remove" << "@ac3");
m_mpv->command( QStringList() << "af" << "remove" << "@ac3");
}
QVariant device = SettingsComponent::Get().value(SETTINGS_SECTION_AUDIO, "device");
@@ -1142,42 +1247,42 @@ void PlayerComponent::updateSubtitleConfiguration()
void PlayerComponent::setSubtitleConfiguration()
{
bool assScaleBorderAndShadow = SettingsComponent::Get().value(SETTINGS_SECTION_SUBTITLES, "ass_scale_border_and_shadow").toBool();
mpv::qt::set_property(m_mpv, "sub-ass-style-overrides", assScaleBorderAndShadow ? "ScaledBorderAndShadow=yes" : "ScaledBorderAndShadow=no");
m_mpv->setProperty( "sub-ass-style-overrides", assScaleBorderAndShadow ? "ScaledBorderAndShadow=yes" : "ScaledBorderAndShadow=no");
QString assStyleOverride = SettingsComponent::Get().value(SETTINGS_SECTION_SUBTITLES, "ass_style_override").toString();
if (!assStyleOverride.isEmpty())
{
mpv::qt::set_property(m_mpv, "sub-ass-override", assStyleOverride);
m_mpv->setProperty( "sub-ass-override", assStyleOverride);
}
QVariant size = SettingsComponent::Get().value(SETTINGS_SECTION_SUBTITLES, "size");
if (size != -1)
{
mpv::qt::set_property(m_mpv, "sub-scale", size.toInt() / 32.0);
m_mpv->setProperty( "sub-scale", size.toInt() / 32.0);
}
QString font = SettingsComponent::Get().value(SETTINGS_SECTION_SUBTITLES, "font").toString();
if (!font.isEmpty())
{
mpv::qt::set_property(m_mpv, "sub-font", font);
m_mpv->setProperty( "sub-font", font);
}
QString color = SettingsComponent::Get().value(SETTINGS_SECTION_SUBTITLES, "color").toString();
if (!color.isEmpty())
{
mpv::qt::set_property(m_mpv, "sub-color", color);
m_mpv->setProperty( "sub-color", color);
}
QString borderColor = SettingsComponent::Get().value(SETTINGS_SECTION_SUBTITLES, "border_color").toString();
if (!borderColor.isEmpty())
{
mpv::qt::set_property(m_mpv, "sub-border-color", borderColor);
m_mpv->setProperty( "sub-border-color", borderColor);
}
QVariant borderSize = SettingsComponent::Get().value(SETTINGS_SECTION_SUBTITLES, "border_size");
if (borderSize != -1)
{
mpv::qt::set_property(m_mpv, "sub-border-size", borderSize.toInt());
m_mpv->setProperty( "sub-border-size", borderSize.toInt());
}
QString backgroundColor = SettingsComponent::Get().value(SETTINGS_SECTION_SUBTITLES, "background_color").toString();
@@ -1186,15 +1291,15 @@ void PlayerComponent::setSubtitleConfiguration()
{
// Color is #RRGGBB or #AARRGGBB, insert Alpha after # (at position 1)
backgroundColor.insert(1, backgroundTransparency);
mpv::qt::set_property(m_mpv, "sub-back-color", backgroundColor);
m_mpv->setProperty( "sub-back-color", backgroundColor);
}
QVariant subposString = SettingsComponent::Get().value(SETTINGS_SECTION_SUBTITLES, "placement");
auto subpos = subposString.toString().split(",");
if (subpos.length() == 2)
{
mpv::qt::set_property(m_mpv, "sub-align-x", subpos[0]);
mpv::qt::set_property(m_mpv, "sub-pos", subpos[1] == "bottom" ? 100 : 10);
m_mpv->setProperty( "sub-align-x", subpos[0]);
m_mpv->setProperty( "sub-pos", subpos[1] == "bottom" ? 100 : 10);
}
}
@@ -1225,7 +1330,7 @@ void PlayerComponent::updateVideoAspectSettings()
}
else if (mode == "force_16_9_if_4_3")
{
auto params = mpv::qt::get_property(m_mpv, "video-dec-params").toMap();
auto params = m_mpv->getProperty( "video-dec-params").toMap();
auto aspect = params["aspect"].toFloat();
if (fabs(aspect - 4.0/3.0) < 0.1)
forceAspect = "16:9";
@@ -1239,10 +1344,10 @@ void PlayerComponent::updateVideoAspectSettings()
disableScaling = true;
}
mpv::qt::set_property(m_mpv, "video-unscaled", disableScaling);
mpv::qt::set_property(m_mpv, "video-aspect-override", forceAspect);
mpv::qt::set_property(m_mpv, "keepaspect", keepAspect);
mpv::qt::set_property(m_mpv, "panscan", panScan);
m_mpv->setProperty( "video-unscaled", disableScaling);
m_mpv->setProperty( "video-aspect-override", forceAspect);
m_mpv->setProperty( "keepaspect", keepAspect);
m_mpv->setProperty( "panscan", panScan);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -1259,7 +1364,7 @@ void PlayerComponent::setVideoConfiguration()
return;
QVariant syncMode = SettingsComponent::Get().value(SETTINGS_SECTION_VIDEO, "sync_mode");
mpv::qt::set_property(m_mpv, "video-sync", syncMode);
m_mpv->setProperty( "video-sync", syncMode);
QString hardwareDecodingMode = SettingsComponent::Get().value(SETTINGS_SECTION_VIDEO, "hardwareDecoding").toString();
QString hwdecMode = "no";
@@ -1275,21 +1380,21 @@ void PlayerComponent::setVideoConfiguration()
{
hwdecMode = "auto-copy";
}
mpv::qt::set_property(m_mpv, "hwdec", hwdecMode);
mpv::qt::set_property(m_mpv, "hwdec-image-format", hwdecVTFormat);
m_mpv->setProperty( "hwdec", hwdecMode);
m_mpv->setProperty( "hwdec-image-format", hwdecVTFormat);
QVariant deinterlace = SettingsComponent::Get().value(SETTINGS_SECTION_VIDEO, "deinterlace");
mpv::qt::set_property(m_mpv, "deinterlace", deinterlace.toBool() ? "yes" : "no");
m_mpv->setProperty( "deinterlace", deinterlace.toBool() ? "yes" : "no");
#ifndef TARGET_RPI
double displayFps = DisplayComponent::Get().currentRefreshRate();
mpv::qt::set_property(m_mpv, "display-fps-override", displayFps);
m_mpv->setProperty( "display-fps-override", displayFps);
#endif
setAudioDelay(m_playbackAudioDelay);
QVariant cache = SettingsComponent::Get().value(SETTINGS_SECTION_VIDEO, "cache");
mpv::qt::set_property(m_mpv, "demuxer-max-bytes", cache.toInt() * 1024 * 1024);
m_mpv->setProperty( "demuxer-max-bytes", cache.toInt() * 1024 * 1024);
updateVideoAspectSettings();
setOtherConfiguration();
@@ -1301,7 +1406,7 @@ void PlayerComponent::setOtherConfiguration()
{
QString otherConfiguration = SettingsComponent::Get().value(SETTINGS_SECTION_OTHER, "other_conf").toString();
qDebug() << "Parsing other configuration: "+otherConfiguration;
QStringList configurationList = otherConfiguration.split(QRegExp("[\r\n]"), Qt::SkipEmptyParts);
QStringList configurationList = otherConfiguration.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts);
for(const QString& configuration : configurationList)
{
@@ -1309,9 +1414,9 @@ void PlayerComponent::setOtherConfiguration()
int configurationLength = configuration.length();
if (splitIndex > 0 && splitIndex < configurationLength - 1)
{
QString configurationKey = configuration.left(splitIndex).remove(QRegExp("^([\"]+)")).remove(QRegExp("([\"]+)$"));
QString configurationValue = configuration.right(configurationLength - splitIndex - 1).remove(QRegExp("^([\"]+)")).remove(QRegExp("([\"]+)$"));
mpv::qt::set_property(m_mpv, configurationKey, configurationValue);
QString configurationKey = configuration.left(splitIndex).remove(QRegularExpression("^([\"]+)")).remove(QRegularExpression("([\"]+)$"));
QString configurationValue = configuration.right(configurationLength - splitIndex - 1).remove(QRegularExpression("^([\"]+)")).remove(QRegularExpression("([\"]+)$"));
m_mpv->setProperty( configurationKey, configurationValue);
}
}
}
@@ -1329,7 +1434,7 @@ void PlayerComponent::updateConfiguration()
void PlayerComponent::userCommand(QString command)
{
QByteArray cmdUtf8 = command.toUtf8();
mpv_command_string(m_mpv, cmdUtf8.data());
mpv_command_string(m_mpv->mpv(), cmdUtf8.data());
}
/////////////////////////////////////////////////////////////////////////////////////////
@@ -1393,8 +1498,8 @@ QList<CodecDriver> PlayerComponent::installedCodecDrivers()
{
QList<CodecDriver> codecs;
codecs.append(convertCodecList(mpv::qt::get_property(m_mpv, "decoder-list"), CodecType::Decoder));
codecs.append(convertCodecList(mpv::qt::get_property(m_mpv, "encoder-list"), CodecType::Encoder));
codecs.append(convertCodecList(m_mpv->getProperty( "decoder-list"), CodecType::Decoder));
codecs.append(convertCodecList(m_mpv->getProperty( "encoder-list"), CodecType::Encoder));
return codecs;
}
@@ -1439,7 +1544,7 @@ PlaybackInfo PlayerComponent::getPlaybackInfo()
info.enableAC3Transcoding = m_doAc3Transcoding;
auto tracks = mpv::qt::get_property(m_mpv, "track-list");
auto tracks = m_mpv->getProperty( "track-list");
for (auto track : tracks.toList())
{
QVariantMap map = track.toMap();
@@ -1515,8 +1620,8 @@ void PlayerComponent::setPreferredCodecs(const QList<CodecDriver>& codecs)
QString opt = items.join(",");
// For simplicity, we don't distinguish between audio and video. The player
// will ignore entries with mismatching media type.
mpv::qt::set_property(m_mpv, "ad", opt);
mpv::qt::set_property(m_mpv, "vd", opt);
m_mpv->setProperty( "ad", opt);
m_mpv->setProperty( "vd", opt);
}
// For QVariant.
@@ -1555,8 +1660,8 @@ static QString get_mpv_osd(mpv_handle *ctx, const QString& property)
return r;
}
#define MPV_PROPERTY(p) get_mpv_osd(m_mpv, p)
#define MPV_PROPERTY_BOOL(p) (mpv::qt::get_property(m_mpv, p).toBool())
#define MPV_PROPERTY(p) get_mpv_osd(m_mpv->mpv(), p)
#define MPV_PROPERTY_BOOL(p) (m_mpv->getProperty(p).toBool())
/////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::appendAudioFormat(QTextStream& info, const QString& property) const
@@ -1585,7 +1690,7 @@ QString PlayerComponent::videoInformation() const
QTextStream info(&infoStr);
// check if video is playing
if (mpv::qt::get_property(m_mpv, "idle-active").toBool())
if (m_mpv->getProperty( "idle-active").toBool())
return "";
info << "File:\n";
@@ -1604,7 +1709,7 @@ QString PlayerComponent::videoInformation() const
info << "Aspect: " << MPV_PROPERTY("video-params/aspect") << "\n";
info << "Bitrate: " << MPV_PROPERTY("video-bitrate") << "\n";
double displayFps = DisplayComponent::Get().currentRefreshRate();
info << "Display FPS: " << MPV_PROPERTY("display-fps")
info << "Display FPS: " << MPV_PROPERTY("override-display-fps")
<< " (" << displayFps << ")" << "\n";
info << "Hardware Decoding: " << MPV_PROPERTY("hwdec-current")
<< " (" << MPV_PROPERTY("hwdec-interop") << ")\n";
+14 -9
View File
@@ -17,6 +17,7 @@
#include <mpv/client.h>
class MpvController;
///////////////////////////////////////////////////////////////////////////////////////////////////
class PlayerComponent : public ComponentBase
@@ -34,14 +35,14 @@ public:
~PlayerComponent() override;
// Deprecated. Corresponds to stop() + queueMedia().
Q_INVOKABLE bool load(const QString& url, const QVariantMap& options, const QVariantMap& metadata, const QString& audioStream = QString(), const QString& subtitleStream = QString());
Q_INVOKABLE bool load(const QString& url, const QVariantMap& options, const QVariantMap& metadata, const QVariant& audioStream = QVariant(), const QVariant& subtitleStream = QVariant());
// Append a media item to the internal playlist. If nothing is played yet, the
// newly appended item will start playing immediately.
// options:
// - startMilliseconds: start playback at this time (in ms)
// - autoplay: if false, start playback paused; if true, start normally
Q_INVOKABLE void queueMedia(const QString& url, const QVariantMap& options, const QVariantMap &metadata, const QString& audioStream, const QString& subtitleStream);
Q_INVOKABLE void queueMedia(const QString& url, const QVariantMap& options, const QVariantMap &metadata, const QVariant& audioStream, const QVariant& subtitleStream);
// This clears all items queued with queueMedia().
// It explicitly excludes the currently playing item. The main use of this function
@@ -82,8 +83,8 @@ public:
// Uses the "name" from the device list.
Q_INVOKABLE virtual void setAudioDevice(const QString& name);
Q_INVOKABLE virtual void setAudioStream(const QString& audioStream);
Q_INVOKABLE virtual void setSubtitleStream(const QString& subtitleStream);
Q_INVOKABLE virtual void setAudioStream(const QVariant& audioStream);
Q_INVOKABLE virtual void setSubtitleStream(const QVariant& subtitleStream);
Q_INVOKABLE virtual void setAudioDelay(qint64 milliseconds);
Q_INVOKABLE virtual void setSubtitleDelay(qint64 milliseconds);
@@ -124,7 +125,11 @@ public:
QRect videoRectangle() { return m_videoRectangle; }
const mpv::qt::Handle getMpvHandle() const { return m_mpv; }
void setMpvController(MpvController* controller) {
if (!m_mpv)
m_mpv = controller;
}
void initializeMpv();
virtual void setWindow(QQuickWindow* window);
@@ -221,9 +226,9 @@ private:
void startCodecsLoading(std::function<void()> resume);
void updateVideoAspectSettings();
QVariantList findStreamsForURL(const QString &url);
void reselectStream(const QString &streamSelection, MediaType target);
void reselectStream(const QVariant &streamSelection, MediaType target);
mpv::qt::Handle m_mpv;
MpvController* m_mpv = nullptr;
State m_state;
bool m_paused;
@@ -247,8 +252,8 @@ private:
bool m_doAc3Transcoding;
QStringList m_passthroughCodecs;
QVariantMap m_serverMediaInfo;
QString m_currentSubtitleStream;
QString m_currentAudioStream;
QVariant m_currentSubtitleStream;
QVariant m_currentAudioStream;
QRect m_videoRectangle;
};
-338
View File
@@ -1,338 +0,0 @@
#include "PlayerQuickItem.h"
#include <stdexcept>
#include <QCoreApplication>
#include <QGuiApplication>
#include <QOpenGLContext>
#include <QRunnable>
#include <QtGui/QOpenGLFramebufferObject>
#include <QtQuick/QQuickWindow>
#include <QOpenGLFunctions>
#include <mpv/render_gl.h>
#include "utils/Utils.h"
#if defined(Q_OS_WIN32)
#include <windows.h>
#include <dwmapi.h>
#include <avrt.h>
#endif
#ifdef USE_X11EXTRAS
#include <QX11Info>
#include <qpa/qplatformnativeinterface.h>
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
static void* get_proc_address(void* ctx, const char* name)
{
Q_UNUSED(ctx);
QOpenGLContext* glctx = QOpenGLContext::currentContext();
if (!glctx)
return nullptr;
void *res = (void *)glctx->getProcAddress(QByteArray(name));
#ifdef Q_OS_WIN32
// wglGetProcAddress(), which is used by Qt, does not always resolve all
// builtin functions with all drivers (only extensions). Qt compensates this
// for a degree, but does this only for functions Qt happens to need. So
// we need our own falback as well.
if (!res)
{
HMODULE handle = (HMODULE)QOpenGLContext::openGLModuleHandle();
if (handle)
res = (void *)GetProcAddress(handle, name);
}
#endif
return res;
}
namespace {
/////////////////////////////////////////////////////////////////////////////////////////
class RequestRepaintJob : public QRunnable
{
public:
explicit RequestRepaintJob(QQuickWindow *window) : m_window(window) { }
void run() override
{
// QSGThreadedRenderLoop::update has a special code path that will render
// without syncing the render and GUI threads unless asked elsewhere to support
// QQuickAnimator animations. This is currently triggered by the fact that
// QQuickWindow::update() is called from the render thread.
// This allows continuing rendering video while the GUI thread is busy.
//
m_window->update();
}
private:
QQuickWindow *m_window;
};
}
///////////////////////////////////////////////////////////////////////////////////////////////////
PlayerRenderer::PlayerRenderer(mpv::qt::Handle mpv, QQuickWindow* window)
: m_mpv(mpv), m_mpvGL(nullptr), m_window(window), m_size(), m_hAvrtHandle(nullptr), m_videoRectangle(-1, -1, -1, -1), m_fbo(0)
{
}
///////////////////////////////////////////////////////////////////////////////////////////////////
bool PlayerRenderer::init()
{
#ifdef Q_OS_WIN32
// Request Multimedia Class Schedule Service.
DwmEnableMMCSS(TRUE);
#endif
mpv_opengl_init_params opengl_params = {
#ifdef Q_OS_WIN32
get_proc_address,
NULL,
#else
.get_proc_address = get_proc_address,
.get_proc_address_ctx = NULL,
#endif
};
const QString platformName = QGuiApplication::platformName();
mpv_render_param params[] = {
{MPV_RENDER_PARAM_API_TYPE, (void*)MPV_RENDER_API_TYPE_OPENGL},
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &opengl_params},
{MPV_RENDER_PARAM_INVALID},
{MPV_RENDER_PARAM_INVALID},
};
#ifdef USE_X11EXTRAS
if (platformName.contains("xcb")) {
params[2].type = MPV_RENDER_PARAM_X11_DISPLAY;
params[2].data = QX11Info::display();
} else if (platformName.contains("wayland")) {
QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface();
params[2].type = MPV_RENDER_PARAM_WL_DISPLAY;
params[2].data = native->nativeResourceForWindow("display", NULL);
}
#endif
int err = mpv_render_context_create(&m_mpvGL, m_mpv, params);
if (err >= 0) {
mpv_render_context_set_update_callback(m_mpvGL, on_update, (void *)this);
return true;
}
return false;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
PlayerRenderer::~PlayerRenderer()
{
// Keep in mind that the m_mpv handle must be held until this is done.
if (m_mpvGL)
mpv_render_context_free(m_mpvGL);
m_mpvGL = nullptr;
delete m_fbo;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerRenderer::render()
{
QOpenGLContext *context = QOpenGLContext::currentContext();
GLint fbo = 0;
context->functions()->glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo);
bool flip = true;
#if HAVE_OPTIMALORIENTATION
flip = !(context->format().orientationFlags() & QSurfaceFormat::MirrorVertically);
#endif
bool screenFlip = flip;
QSize fboSize = m_size;
QOpenGLFramebufferObject *blitFbo = 0;
m_window->resetOpenGLState();
QRect fullWindow(0, 0, m_size.width(), m_size.height());
if (m_videoRectangle.width() > 0 && m_videoRectangle.height() > 0 && m_videoRectangle != fullWindow && QOpenGLFramebufferObject::hasOpenGLFramebufferBlit() && QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
{
if (!m_fbo || !m_fbo->isValid() || m_fbo->size() != m_videoRectangle.size())
{
delete m_fbo;
m_fbo = new QOpenGLFramebufferObject(m_videoRectangle.size());
}
if (m_fbo && m_fbo->isValid())
{
blitFbo = m_fbo;
fboSize = m_fbo->size();
fbo = m_fbo->handle();
flip = false;
// Need to clear the background manually, since nothing else knows it has to be done.
context->functions()->glClearColor(0, 0, 0, 0);
context->functions()->glClear(GL_COLOR_BUFFER_BIT);
}
}
mpv_opengl_fbo mpv_fbo = {
#ifdef Q_OS_WIN32
fbo,
fboSize.width(),
fboSize.height(),
#else
.fbo = fbo,
.w = fboSize.width(),
.h = fboSize.height(),
#endif
};
int mpv_flip = flip ? -1 : 0;
mpv_render_param params[] = {
{MPV_RENDER_PARAM_OPENGL_FBO, &mpv_fbo},
{MPV_RENDER_PARAM_FLIP_Y, &mpv_flip},
{MPV_RENDER_PARAM_INVALID}
};
mpv_render_context_render(m_mpvGL, params);
m_window->resetOpenGLState();
if (blitFbo)
{
QRect dstRect = m_videoRectangle;
if (screenFlip)
dstRect = QRect(dstRect.x(), m_size.height() - dstRect.y(), dstRect.width(), dstRect.top() - dstRect.bottom());
QOpenGLFramebufferObject::blitFramebuffer(0, dstRect, blitFbo, QRect(QPoint(0, 0), blitFbo->size()));
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerRenderer::swap()
{
if (m_mpvGL)
mpv_render_context_report_swap(m_mpvGL);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerRenderer::onVideoPlaybackActive(bool active)
{
#ifdef Q_OS_WIN32
if (active && !m_hAvrtHandle)
{
DWORD handle = 0;
m_hAvrtHandle = AvSetMmThreadCharacteristicsW(L"Low Latency", &handle);
}
else if (!active && m_hAvrtHandle)
{
AvRevertMmThreadCharacteristics(m_hAvrtHandle);
m_hAvrtHandle = 0;
}
#endif
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerRenderer::on_update(void *ctx)
{
PlayerRenderer *self = (PlayerRenderer *)ctx;
// QQuickWindow::scheduleRenderJob is expected to be called from the GUI thread but
// is thread-safe when using the QSGThreadedRenderLoop. We can detect a non-threaded render
// loop by checking if QQuickWindow::beforeSynchronizing was called from the GUI thread
// (which affects the QObject::thread() of the PlayerRenderer).
//
if (self->thread() == self->m_window->thread())
QMetaObject::invokeMethod(self->m_window, "update", Qt::QueuedConnection);
else
self->m_window->scheduleRenderJob(new RequestRepaintJob(self->m_window), QQuickWindow::NoStage);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
PlayerQuickItem::PlayerQuickItem(QQuickItem* parent)
: QQuickItem(parent), m_mpvGL(nullptr), m_renderer(nullptr)
{
connect(this, &QQuickItem::windowChanged, this, &PlayerQuickItem::onWindowChanged, Qt::DirectConnection);
connect(this, &PlayerQuickItem::onFatalError, this, &PlayerQuickItem::onHandleFatalError, Qt::QueuedConnection);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
PlayerQuickItem::~PlayerQuickItem()
{
if (m_mpvGL)
mpv_render_context_set_update_callback(m_mpvGL, nullptr, nullptr);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerQuickItem::onWindowChanged(QQuickWindow* win)
{
if (win)
{
connect(win, &QQuickWindow::beforeSynchronizing, this, &PlayerQuickItem::onSynchronize, Qt::DirectConnection);
connect(win, &QQuickWindow::sceneGraphInvalidated, this, &PlayerQuickItem::onInvalidate, Qt::DirectConnection);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerQuickItem::onHandleFatalError(QString message)
{
throw FatalException(message);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerQuickItem::onSynchronize()
{
if (!m_renderer && m_mpv)
{
m_renderer = new PlayerRenderer(m_mpv, window());
if (!m_renderer->init())
{
delete m_renderer;
m_renderer = nullptr;
emit onFatalError(tr("Could not initialize OpenGL."));
return;
}
connect(window(), &QQuickWindow::beforeRendering, m_renderer, &PlayerRenderer::render, Qt::DirectConnection);
connect(window(), &QQuickWindow::frameSwapped, m_renderer, &PlayerRenderer::swap, Qt::DirectConnection);
connect(&PlayerComponent::Get(), &PlayerComponent::videoPlaybackActive, m_renderer, &PlayerRenderer::onVideoPlaybackActive, Qt::QueuedConnection);
connect(&PlayerComponent::Get(), &PlayerComponent::onVideoRecangleChanged, window(), &QQuickWindow::update, Qt::QueuedConnection);
window()->setPersistentOpenGLContext(true);
window()->setPersistentSceneGraph(true);
window()->setClearBeforeRendering(false);
m_debugInfo = "";
QOpenGLContext* glctx = QOpenGLContext::currentContext();
if (glctx && glctx->isValid())
{
m_debugInfo += "\nOpenGL:\n";
int syms[4] = {GL_VENDOR, GL_RENDERER, GL_VERSION, GL_SHADING_LANGUAGE_VERSION};
for (auto sym : syms)
{
auto s = (char *)glctx->functions()->glGetString(sym);
if (s)
m_debugInfo += QString(" ") + QString::fromUtf8(s) + "\n";
}
m_debugInfo += "\n";
}
}
if (m_renderer)
{
m_renderer->m_size = window()->size() * window()->devicePixelRatio();
m_renderer->m_videoRectangle = PlayerComponent::Get().videoRectangle();
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerQuickItem::onInvalidate()
{
if (m_renderer)
delete m_renderer;
m_renderer = nullptr;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerQuickItem::initMpv(PlayerComponent* player)
{
m_mpv = player->getMpvHandle();
connect(player, &PlayerComponent::windowVisible, this, &QQuickItem::setVisible);
window()->update();
}
-72
View File
@@ -1,72 +0,0 @@
#ifndef PLAYERQUICKITEM_H
#define PLAYERQUICKITEM_H
#include <Qt>
#include <QtQuick/QQuickItem>
#include <QOpenGLFramebufferObject>
#include <mpv/client.h>
#include <mpv/render.h>
#ifdef Q_OS_WIN32
#include <windows.h>
#else
typedef void* HANDLE;
#endif
#include "PlayerComponent.h"
#include "QtHelper.h"
class PlayerRenderer : public QObject
{
Q_OBJECT
friend class PlayerQuickItem;
PlayerRenderer(mpv::qt::Handle mpv, QQuickWindow* window);
bool init();
~PlayerRenderer() override;
void render();
void swap();
public slots:
void onVideoPlaybackActive(bool active);
private:
static void on_update(void *ctx);
mpv::qt::Handle m_mpv;
mpv_render_context* m_mpvGL;
QQuickWindow* m_window;
QSize m_size;
HANDLE m_hAvrtHandle;
QRect m_videoRectangle;
QOpenGLFramebufferObject* m_fbo;
};
class PlayerQuickItem : public QQuickItem
{
Q_OBJECT
friend class PlayerRenderer;
public:
explicit PlayerQuickItem(QQuickItem* parent = nullptr);
~PlayerQuickItem() override;
void initMpv(PlayerComponent* player);
QString debugInfo() { return m_debugInfo; }
signals:
void onFatalError(QString message);
private slots:
void onWindowChanged(QQuickWindow* win);
void onSynchronize();
void onInvalidate();
void onHandleFatalError(QString message);
private:
mpv::qt::Handle m_mpv;
mpv_render_context* m_mpvGL;
PlayerRenderer* m_renderer;
QString m_debugInfo;
};
#endif
+1 -5
View File
@@ -136,11 +136,7 @@ private:
return r;
}
bool test_type(const QVariant &v, QMetaType::Type t) {
// The Qt docs say: "Although this function is declared as returning
// "QVariant::Type(obsolete), the return value should be interpreted
// as QMetaType::Type."
// So a cast really seems to be needed to avoid warnings (urgh).
return static_cast<int>(v.type()) == static_cast<int>(t);
return v.typeId() == t;
}
void set(mpv_node *dst, const QVariant &src) {
if (test_type(src, QMetaType::QString)) {
+3 -3
View File
@@ -1,6 +1,6 @@
#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusInterface>
#include <QtDBus/QDBusReply>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusReply>
#include "PowerComponentDBus.h"
+10 -6
View File
@@ -51,12 +51,12 @@ void SettingsComponent::cycleSettingCommand(const QString& args)
// If no possible values are defined, check the type of the default value.
// In the case it's a boolean simply negate the current value to cycle through.
// Otherwise log an error message, that it's not possible to cycle through the value.
if (values.size() == 0)
if (values.empty())
{
if (section->defaultValue(valueName).type() == QVariant::Bool)
if (section->defaultValue(valueName).typeId()== QMetaType::Bool)
{
QVariant currentValue = section->value(valueName);
auto nextValue = currentValue.toBool() ? false : true;
auto nextValue = !currentValue.toBool();
setValue(sectionID, valueName, nextValue);
qDebug() << "Setting" << settingName << "to " << (nextValue ? "Enabled" : "Disabled");
emit SystemComponent::Get().settingsMessage(valueName, nextValue ? "Enabled" : "Disabled");
@@ -178,7 +178,11 @@ QVariant SettingsComponent::orderedSections()
static void writeFile(const QString& filename, const QByteArray& data)
{
QSaveFile file(filename);
file.open(QIODevice::WriteOnly | QIODevice::Text);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
{
qCritical() << "Could not open" << filename << "for writing";
return;
}
file.write(data);
if (!file.commit())
{
@@ -386,7 +390,7 @@ void SettingsComponent::setValues(const QVariantMap& options)
QString key = options["key"].toString();
QVariant values = options["value"];
if (values.type() == QVariant::Map || values.isNull())
if (values.typeId() == QMetaType::QVariantMap || values.isNull())
{
SettingsSection* section = getSection(key);
if (!section)
@@ -405,7 +409,7 @@ void SettingsComponent::setValues(const QVariantMap& options)
saveSection(section);
}
else if (values.type() == QVariant::String)
else if (values.typeId() == QMetaType::QString)
{
setValue(SETTINGS_SECTION_WEBCLIENT, key, values);
}
+18
View File
@@ -116,6 +116,24 @@ bool SettingsSection::setValue(const QString& key, const QVariant& value)
return true;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void SettingsSection::setValueNoSave(const QString& key, const QVariant& value)
{
if (key == "index" || !m_values.contains(key))
return;
if (m_values[key]->value() == value)
return;
m_values[key]->setValue(value);
QVariantMap updatedValues;
updatedValues[key] = value;
emit valuesUpdated(updatedValues);
emit SettingsComponent::Get().sectionValueUpdate(m_sectionID, updatedValues);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void SettingsSection::resetValueNoNotify(const QString& key, QVariantMap& updatedValues)
{
+1
View File
@@ -20,6 +20,7 @@ public:
void setValues(const QVariant& values);
bool setValue(const QString& key, const QVariant& value);
void setValueNoSave(const QString& key, const QVariant& value);
void resetValue(const QString& key);
void resetValues();
void registerSetting(SettingsValue* value);
+16
View File
@@ -9,6 +9,22 @@ add_library(shared STATIC
${CMAKE_CURRENT_BINARY_DIR}/Names.cpp Names.h
)
# GuiPrivate only needed for RPI builds
if(BUILD_TARGET STREQUAL "RPI")
find_package(Qt6 REQUIRED COMPONENTS GuiPrivate)
endif()
target_link_libraries(shared
Qt6::Core
Qt6::Network
Qt6::Qml
Qt6::WebChannel
Qt6::Gui
)
if(BUILD_TARGET STREQUAL "RPI")
target_link_libraries(shared Qt6::GuiPrivate)
endif()
std_target_properties(shared)
clang_tidy(shared)
+1
View File
@@ -6,6 +6,7 @@
#define KONVERGO_LOCALJSONCLIENT_H
#include <QLocalSocket>
#include <QVariant>
class LocalJsonClient : public QLocalSocket
{
+6 -5
View File
@@ -579,11 +579,12 @@ QString SystemComponent::getNativeShellScript()
QFile flatpakOsFile {"/run/host/os-release"};
if (flatpakOsFile.exists()) {
qDebug() << "Found flatpak os-release file";
flatpakOsFile.open(QIODevice::ReadOnly);
QString flatpakOsFileString = QTextStream(&flatpakOsFile).readAll();
if (flatpakOsFileString.contains("NAME=\"SteamOS\"")) {
qDebug() << "Detected SteamOS";
defaultMode = "tv";
if (flatpakOsFile.open(QIODevice::ReadOnly)) {
QString flatpakOsFileString = QTextStream(&flatpakOsFile).readAll();
if (flatpakOsFileString.contains("NAME=\"SteamOS\"")) {
qDebug() << "Detected SteamOS";
defaultMode = "tv";
}
}
}
clientData.insert("mode", QJsonValue::fromVariant(defaultMode));
+2 -2
View File
@@ -6,7 +6,7 @@
#include "system/SystemComponent.h"
#include "settings/SettingsComponent.h"
#include "input/InputKeyboard.h"
#include "KonvergoWindow.h"
#include <QQuickWindow>
#include <QQuickItem>
#include <QKeyEvent>
@@ -64,7 +64,7 @@ static QString keyEventToKeyString(QKeyEvent *kevent)
///////////////////////////////////////////////////////////////////////////////////////////////////
bool EventFilter::eventFilter(QObject* watched, QEvent* event)
{
KonvergoWindow* window = qobject_cast<KonvergoWindow*>(parent());
QQuickWindow* window = qobject_cast<QQuickWindow*>(parent());
if (window && window->property("webDesktopMode").toBool())
{
+10 -8
View File
@@ -15,7 +15,7 @@
#include "settings/SettingsSection.h"
#include "system/SystemComponent.h"
#include "player/PlayerComponent.h"
#include "player/PlayerQuickItem.h"
#include "player/MpvVideoItem.h"
#include "display/DisplayComponent.h"
#include "taskbar/TaskbarComponent.h"
#include "utils/Utils.h"
@@ -23,7 +23,7 @@
#include "EventFilter.h"
#ifdef USE_X11EXTRAS
#include <QX11Info>
#include <QGuiApplication>
#include <X11/Xlib.h>
#endif
@@ -76,9 +76,11 @@ KonvergoWindow::KonvergoWindow(QWindow* parent) :
#ifdef USE_X11EXTRAS
// On Gnome show a darker title bar
if (QX11Info::isPlatformX11())
if (QGuiApplication::platformName() == "xcb")
{
Display* dpy = QX11Info::display();
QNativeInterface::QX11Application *x11AppInfo = qApp->nativeInterface<QNativeInterface::QX11Application>();
Display* dpy = x11AppInfo->display();
if (dpy)
{
WId win = winId();
@@ -118,7 +120,7 @@ KonvergoWindow::KonvergoWindow(QWindow* parent) :
// this is using old syntax because ... reasons. QQuickCloseEvent is not public class
connect(this, SIGNAL(closing(QQuickCloseEvent*)), this, SLOT(closingWindow()));
connect(qApp, &QCoreApplication::aboutToQuit, this, &KonvergoWindow::closingWindow);
connect(qApp, &QCoreApplication::aboutToQuit, this, &KonvergoWindow::saveGeometry);
#ifdef Q_OS_MAC
m_osxPresentationOptions = 0;
@@ -144,6 +146,7 @@ void KonvergoWindow::closingWindow()
if (!SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "fullscreen").toBool())
saveGeometry();
qDebug() << "Quitting application...";
qApp->quit();
}
@@ -538,9 +541,8 @@ void KonvergoWindow::updateDebugInfo()
m_systemDebugInfo = SystemComponent::Get().debugInformation();
m_debugInfo = m_systemDebugInfo;
m_debugInfo += DisplayComponent::Get().debugInformation();
PlayerQuickItem* video = findChild<PlayerQuickItem*>("video");
if (video)
m_debugInfo += video->debugInfo();
MpvVideoItem* video = findChild<MpvVideoItem*>("video");
Q_UNUSED(video); // MpvQt handles debug info internally, skip for now
QString infoString;
QDebug info(&infoString);
info << "Qt windowing info:\n";
+2 -1
View File
@@ -60,8 +60,9 @@ public:
Q_SLOT void otherAppFocus()
{
setWindowState((Qt::WindowState)((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive));
setWindowState((Qt::WindowState)(windowState() & ~Qt::WindowMinimized));
raise();
requestActivate();
}
Q_SLOT void toggleDebug();
+605
View File
@@ -0,0 +1,605 @@
#include "WindowManager.h"
#include "core/Globals.h"
#include "settings/SettingsComponent.h"
#include "settings/SettingsSection.h"
#include "player/PlayerComponent.h"
#include "display/DisplayComponent.h"
#include "taskbar/TaskbarComponent.h"
#include "input/InputComponent.h"
#include <QGuiApplication>
#include <QScreen>
#include <QDebug>
///////////////////////////////////////////////////////////////////////////////////////////////////
WindowManager& WindowManager::Get()
{
static WindowManager instance;
return instance;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
WindowManager::WindowManager(QObject* parent)
: ComponentBase(parent),
m_window(nullptr),
m_ignoreFullscreenSettingsChange(0),
m_maximized(false),
m_fullscreen(false),
m_geometryChangeTimer(nullptr)
{
}
///////////////////////////////////////////////////////////////////////////////////////////////////
WindowManager::~WindowManager()
{
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::componentPostInitialize()
{
// Window not available yet - will be initialized via initializeWindow()
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::initializeWindow(QQuickWindow* window)
{
m_window = window;
if (!m_window)
{
qCritical() << "WindowManager: Failed to get main window";
return;
}
// Initialize components that need window reference
PlayerComponent::Get().setWindow(m_window);
DisplayComponent::Get().setApplicationWindow(m_window);
TaskbarComponent::Get().setWindow(m_window);
// Register host command for fullscreen toggle
InputComponent::Get().registerHostCommand("fullscreen", this, "toggleFullscreen");
// Load and apply saved geometry
loadGeometry();
// Connect to settings
connectSettings();
// Apply initial settings
applySettings();
// Setup screen management
updateScreens();
// Setup timer for delayed geometry updates (avoid saving during maximize transitions)
m_geometryChangeTimer = new QTimer(this);
m_geometryChangeTimer->setSingleShot(true);
m_geometryChangeTimer->setInterval(200); // 200ms delay
connect(m_geometryChangeTimer, &QTimer::timeout, this, [this]() {
// Only commit if still in windowed mode after delay
if (m_window->visibility() == QWindow::Windowed && !m_maximized && !m_fullscreen)
m_normalGeometry = m_pendingGeometry;
});
// Connect to window signals
connect(m_window, &QQuickWindow::visibilityChanged,
this, &WindowManager::onVisibilityChanged);
// Track geometry changes with delay
connect(m_window, &QQuickWindow::widthChanged, this, [this]() {
if (m_window->visibility() == QWindow::Windowed && !m_maximized && !m_fullscreen)
{
m_pendingGeometry = m_window->geometry();
m_geometryChangeTimer->start(); // Restart timer
}
});
connect(m_window, &QQuickWindow::heightChanged, this, [this]() {
if (m_window->visibility() == QWindow::Windowed && !m_maximized && !m_fullscreen)
{
m_pendingGeometry = m_window->geometry();
m_geometryChangeTimer->start();
}
});
connect(m_window, &QQuickWindow::xChanged, this, [this]() {
if (m_window->visibility() == QWindow::Windowed && !m_maximized && !m_fullscreen)
{
m_pendingGeometry = m_window->geometry();
m_geometryChangeTimer->start();
}
});
connect(m_window, &QQuickWindow::yChanged, this, [this]() {
if (m_window->visibility() == QWindow::Windowed && !m_maximized && !m_fullscreen)
{
m_pendingGeometry = m_window->geometry();
m_geometryChangeTimer->start();
}
});
// Connect to application shutdown
connect(qApp, &QGuiApplication::aboutToQuit,
this, &WindowManager::saveGeometrySlot);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::setAlwaysOnTop(bool enable)
{
if (!m_window)
return;
Qt::WindowFlags flags = m_window->flags();
Qt::WindowFlags onTopFlags = Qt::WindowStaysOnTopHint;
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
// X11 needs bypass hint, Wayland doesn't support it
if (QGuiApplication::platformName() == "xcb")
onTopFlags |= Qt::X11BypassWindowManagerHint;
#endif
if (enable)
m_window->setFlags(flags | onTopFlags);
else
m_window->setFlags(flags & ~onTopFlags);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
bool WindowManager::isAlwaysOnTop() const
{
if (!m_window)
return false;
Qt::WindowFlags checkFlags = Qt::WindowStaysOnTopHint;
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (QGuiApplication::platformName() == "xcb")
checkFlags |= Qt::X11BypassWindowManagerHint;
#endif
return (m_window->flags() & checkFlags) == checkFlags;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::toggleAlwaysOnTop()
{
setAlwaysOnTop(!isAlwaysOnTop());
}
///////////////////////////////////////////////////////////////////////////////////////////////////
bool WindowManager::isWayland() const
{
return QGuiApplication::platformName() == "wayland";
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::setFullScreen(bool enable)
{
if (!m_window)
return;
m_window->setVisibility(enable ? QWindow::FullScreen : QWindow::Windowed);
if (enable)
updateForcedScreen();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
bool WindowManager::isFullScreen() const
{
if (!m_window)
return false;
return m_window->visibility() == QWindow::FullScreen;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::toggleFullscreen()
{
setFullScreen(!isFullScreen());
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::raiseWindow()
{
if (!m_window)
return;
// Restore from minimized state if needed
if (m_window->windowState() & Qt::WindowMinimized)
m_window->setWindowState((Qt::WindowState)(m_window->windowState() & ~Qt::WindowMinimized));
// Raise and request activation
// Note: Wayland blocks requestActivate() for security (prevents focus stealing)
// On Wayland, window will be raised but user must click to activate
m_window->show();
m_window->raise();
m_window->requestActivate();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::onVisibilityChanged(QWindow::Visibility visibility)
{
bool isFS = (visibility == QWindow::FullScreen);
bool wasMaximized = m_maximized;
bool wasFullscreen = m_fullscreen;
// Kiosk mode: force back to fullscreen if user tried to exit
if (!isFS)
{
bool forceAlwaysFS = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "forceAlwaysFS").toBool();
if (forceAlwaysFS)
{
setFullScreen(true);
return;
}
}
// Wayland: manually restore geometry when un-maximizing
if (wasMaximized && visibility == QWindow::Windowed)
{
qDebug() << "Un-maximizing, restoring to m_normalGeometry:" << m_normalGeometry;
m_window->setGeometry(m_normalGeometry);
}
// Update maximized/fullscreen state AFTER geometry restore (but not when Hidden)
if (visibility != QWindow::Hidden)
{
m_maximized = (visibility == QWindow::Maximized);
m_fullscreen = isFS;
}
// Update setting in-memory only (no disk write) when fullscreen state changes
if (m_fullscreen != wasFullscreen && (visibility == QWindow::FullScreen || visibility == QWindow::Windowed))
{
SettingsSection* section = SettingsComponent::Get().getSection(SETTINGS_SECTION_MAIN);
if (section)
{
section->setValueNoSave("fullscreen", m_fullscreen);
}
}
// Save geometry when exiting fullscreen
if (!isFS)
saveGeometry();
// Update window state
updateWindowState(false);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::onScreenAdded(QScreen* screen)
{
if (!screen)
return;
qDebug() << "Screen added:" << screen->name();
// Connect to new screen's signals
connect(screen, &QScreen::geometryChanged,
this, &WindowManager::onScreenGeometryChanged);
connect(screen, &QScreen::logicalDotsPerInchChanged,
this, &WindowManager::onScreenDpiChanged);
updateScreens();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::onScreenRemoved(QScreen* screen)
{
qDebug() << "Screen removed:" << (screen ? screen->name() : "unknown");
updateScreens();
// Check if window was on removed screen
if (m_window && screen && m_currentScreenName == screen->name())
{
qDebug() << "Window was on removed screen, moving to primary";
QScreen* primary = QGuiApplication::primaryScreen();
if (primary)
m_window->setScreen(primary);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::onScreenGeometryChanged(const QRect& geometry)
{
Q_UNUSED(geometry);
qDebug() << "Screen geometry changed";
updateScreens();
updateWindowState();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::onScreenDpiChanged(qreal dpi)
{
Q_UNUSED(dpi);
qDebug() << "Screen DPI changed";
updateScreens();
updateWindowState();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::updateMainSectionSettings(const QVariantMap& values)
{
// Fullscreen
if (values.contains("fullscreen"))
{
bool fs = values["fullscreen"].toBool();
if (fs != isFullScreen())
setFullScreen(fs);
}
// Always on top
if (values.contains("alwaysOnTop"))
{
bool onTop = values["alwaysOnTop"].toBool();
if (onTop != isAlwaysOnTop())
setAlwaysOnTop(onTop);
}
// Web mode
if (values.contains("webMode"))
{
QString mode = values["webMode"].toString();
bool desktopMode = (mode == "desktop");
m_window->setProperty("webDesktopMode", desktopMode);
}
// Forced screen
if (values.contains("forceFSScreen"))
{
if (isFullScreen())
updateForcedScreen();
}
// Always fullscreen
if (values.contains("forceAlwaysFS"))
{
bool alwaysFS = values["forceAlwaysFS"].toBool();
if (alwaysFS && !isFullScreen())
setFullScreen(true);
}
// Startup URL
if (values.contains("startupurl"))
{
QString url = values["startupurl"].toString();
if (!url.isEmpty())
m_window->setProperty("webUrl", url);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::updateWindowState(bool saveGeo)
{
updateCurrentScreen();
if (saveGeo)
saveGeometry();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::saveGeometrySlot()
{
saveGeometry();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::loadGeometry()
{
QRect rect = loadGeometryRect();
// Validate geometry fits in available screens
if (!fitsInScreens(rect))
{
qDebug() << "Saved geometry doesn't fit in available screens, using defaults";
QScreen* primary = QGuiApplication::primaryScreen();
if (primary)
{
QRect screenRect = primary->geometry();
rect = QRect(
screenRect.x() + (screenRect.width() - WEBUI_SIZE.width()) / 2,
screenRect.y() + (screenRect.height() - WEBUI_SIZE.height()) / 2,
WEBUI_SIZE.width(),
WEBUI_SIZE.height()
);
}
else
{
rect = QRect(0, 0, WEBUI_SIZE.width(), WEBUI_SIZE.height());
}
}
// Apply minimum size
if (rect.width() < WINDOWW_MIN_SIZE.width())
rect.setWidth(WINDOWW_MIN_SIZE.width());
if (rect.height() < WINDOWW_MIN_SIZE.height())
rect.setHeight(WINDOWW_MIN_SIZE.height());
// Set geometry
m_window->setGeometry(rect);
// Store as normal geometry
m_normalGeometry = rect;
// Restore maximized state
m_maximized = SettingsComponent::Get().value(SETTINGS_SECTION_STATE, "maximized").toBool();
if (m_maximized)
m_window->setVisibility(QWindow::Maximized);
// Restore to last screen
QScreen* lastScreen = loadLastScreen();
if (lastScreen)
m_window->setScreen(lastScreen);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::saveGeometry()
{
if (!m_window)
return;
// m_normalGeometry is updated by geometry signals, just save it
QString geoStr = QString("%1,%2,%3,%4")
.arg(m_normalGeometry.x()).arg(m_normalGeometry.y())
.arg(m_normalGeometry.width()).arg(m_normalGeometry.height());
SettingsComponent::Get().setValue(SETTINGS_SECTION_STATE, "geometry", geoStr);
SettingsComponent::Get().setValue(SETTINGS_SECTION_STATE, "maximized", m_maximized);
SettingsComponent::Get().setValue(SETTINGS_SECTION_STATE, "lastusedscreen", m_currentScreenName);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
QRect WindowManager::loadGeometryRect()
{
QString geoStr = SettingsComponent::Get().value(SETTINGS_SECTION_STATE, "geometry").toString();
if (geoStr.isEmpty())
return QRect(0, 0, WEBUI_SIZE.width(), WEBUI_SIZE.height());
QStringList parts = geoStr.split(',');
if (parts.size() != 4)
return QRect(0, 0, WEBUI_SIZE.width(), WEBUI_SIZE.height());
return QRect(
parts[0].toInt(),
parts[1].toInt(),
parts[2].toInt(),
parts[3].toInt()
);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
bool WindowManager::fitsInScreens(const QRect& rc)
{
for (QScreen* screen : QGuiApplication::screens())
{
if (screen->geometry().intersects(rc))
return true;
}
return false;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::updateScreens()
{
// Connect to all screen signals
for (QScreen* screen : QGuiApplication::screens())
{
connect(screen, &QScreen::geometryChanged,
this, &WindowManager::onScreenGeometryChanged, Qt::UniqueConnection);
connect(screen, &QScreen::logicalDotsPerInchChanged,
this, &WindowManager::onScreenDpiChanged, Qt::UniqueConnection);
}
// Connect to app screen add/remove
connect(qApp, &QGuiApplication::screenAdded,
this, &WindowManager::onScreenAdded, Qt::UniqueConnection);
connect(qApp, &QGuiApplication::screenRemoved,
this, &WindowManager::onScreenRemoved, Qt::UniqueConnection);
updateCurrentScreen();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::updateCurrentScreen()
{
QScreen* screen = findCurrentScreen();
if (screen)
{
QString newName = screen->name();
if (newName != m_currentScreenName)
{
qDebug() << "Current screen changed:" << m_currentScreenName << "->" << newName;
m_currentScreenName = newName;
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
QScreen* WindowManager::findCurrentScreen()
{
if (!m_window)
return QGuiApplication::primaryScreen();
QRect windowRect = m_window->geometry();
QScreen* bestScreen = nullptr;
int maxIntersect = 0;
for (QScreen* screen : QGuiApplication::screens())
{
QRect intersect = screen->geometry().intersected(windowRect);
int area = intersect.width() * intersect.height();
if (area > maxIntersect)
{
maxIntersect = area;
bestScreen = screen;
}
}
return bestScreen ? bestScreen : QGuiApplication::primaryScreen();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
QScreen* WindowManager::loadLastScreen()
{
QString screenName = SettingsComponent::Get().value(SETTINGS_SECTION_STATE, "lastusedscreen").toString();
if (screenName.isEmpty())
return QGuiApplication::primaryScreen();
for (QScreen* screen : QGuiApplication::screens())
{
if (screen->name() == screenName)
{
qDebug() << "Restored to last screen:" << screenName;
return screen;
}
}
qDebug() << "Last screen not found:" << screenName << "using primary";
return QGuiApplication::primaryScreen();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::updateForcedScreen()
{
QString forcedScreen = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "forceFSScreen").toString();
if (forcedScreen.isEmpty() || !isFullScreen())
return;
for (QScreen* screen : QGuiApplication::screens())
{
if (screen->name() == forcedScreen)
{
qDebug() << "Forcing fullscreen to screen:" << forcedScreen;
m_window->setScreen(screen);
m_window->setVisibility(QWindow::FullScreen);
return;
}
}
qDebug() << "Forced screen not found:" << forcedScreen;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::connectSettings()
{
connect(SettingsComponent::Get().getSection(SETTINGS_SECTION_MAIN),
&SettingsSection::valuesUpdated,
this, &WindowManager::updateMainSectionSettings);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void WindowManager::applySettings()
{
// Read and apply all settings
QVariantMap values;
values["fullscreen"] = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "fullscreen");
values["alwaysOnTop"] = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "alwaysOnTop");
values["webMode"] = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "webMode");
values["forceFSScreen"] = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "forceFSScreen");
values["forceAlwaysFS"] = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "forceAlwaysFS");
values["startupurl"] = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "startupurl");
updateMainSectionSettings(values);
}
+90
View File
@@ -0,0 +1,90 @@
#ifndef WINDOWMANAGER_H
#define WINDOWMANAGER_H
#include <QObject>
#include <QQuickWindow>
#include <QRect>
#include <QScreen>
#include <QTimer>
#include <QWindow>
#include "core/ComponentManager.h"
#define WEBUI_MAX_HEIGHT 1440.0
#define WEBUI_SIZE QSize(1280, 720)
#define WINDOWW_MIN_SIZE QSize(213, 120)
class WindowManager : public ComponentBase
{
Q_OBJECT
public:
static WindowManager& Get();
explicit WindowManager(QObject* parent = nullptr);
~WindowManager() override;
const char* componentName() override { return "window"; }
bool componentExport() override { return true; }
bool componentInitialize() override { return true; }
void componentPostInitialize() override;
// Called from main.cpp after window created
void initializeWindow(QQuickWindow* window);
// Always on top
Q_INVOKABLE void setAlwaysOnTop(bool enable);
Q_INVOKABLE bool isAlwaysOnTop() const;
Q_INVOKABLE void toggleAlwaysOnTop();
// Platform detection
Q_INVOKABLE bool isWayland() const;
// Fullscreen
Q_INVOKABLE void setFullScreen(bool enable);
Q_INVOKABLE bool isFullScreen() const;
// Window activation
Q_INVOKABLE void raiseWindow();
public slots:
void toggleFullscreen();
private slots:
void onVisibilityChanged(QWindow::Visibility visibility);
void onScreenAdded(QScreen* screen);
void onScreenRemoved(QScreen* screen);
void onScreenGeometryChanged(const QRect& geometry);
void onScreenDpiChanged(qreal dpi);
void updateMainSectionSettings(const QVariantMap& values);
void updateWindowState(bool saveGeo = true);
void saveGeometrySlot();
private:
// Geometry
void loadGeometry();
void saveGeometry();
QRect loadGeometryRect();
bool fitsInScreens(const QRect& rc);
// Screens
void updateScreens();
void updateCurrentScreen();
QScreen* findCurrentScreen();
QScreen* loadLastScreen();
void updateForcedScreen();
// Settings
void connectSettings();
void applySettings();
QQuickWindow* m_window;
QString m_currentScreenName;
int m_ignoreFullscreenSettingsChange;
QRect m_normalGeometry;
bool m_maximized;
bool m_fullscreen;
QTimer* m_geometryChangeTimer;
QRect m_pendingGeometry;
};
#endif // WINDOWMANAGER_H
+77 -31
View File
@@ -1,17 +1,53 @@
import QtQuick 2.4
import QtQuick
import Konvergo 1.0
import QtWebEngine 1.7
import QtWebEngine
import QtWebChannel 1.0
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls 6.0
KonvergoWindow
Window
{
id: mainWindow
title: "Jellyfin Media Player"
objectName: "mainWindow"
minimumHeight: windowMinSize.height
minimumWidth: windowMinSize.width
width: 1280
height: 720
minimumWidth: 213
minimumHeight: 120
visible: true
color: "#000000"
// Properties previously from KonvergoWindow
property bool webDesktopMode: true
property bool showDebugLayer: false
property string debugInfo: ""
property string videoInfo: ""
property string webUrl: ""
signal reloadWebClient()
Component.onCompleted: {
if (components && components.settings) {
webUrl = components.settings.getWebClientUrl(webDesktopMode)
}
}
function toggleFullscreen() {
visibility = (visibility === Window.FullScreen) ? Window.Windowed : Window.FullScreen
}
function toggleDebug() {
showDebugLayer = !showDebugLayer
}
function setFullScreen(enable) {
visibility = enable ? Window.FullScreen : Window.Windowed
}
function minimizeWindow() {
if (visibility !== Window.FullScreen)
visibility = Window.Minimized
}
function runWebAction(action)
{
@@ -129,25 +165,38 @@ KonvergoWindow
id: action_forward
}
MpvVideo
{
id: video
objectName: "video"
// It's not a real item. Its renderer draws onto the view's background.
width: 0
height: 0
visible: false
}
WebChannel
{
id: webChannelObject
}
MpvVideoItem
{
id: video
objectName: "video"
enabled: true
width: mainWindow.contentItem.width
height: mainWindow.contentItem.height
anchors.left: mainWindow.contentItem.left
anchors.right: mainWindow.contentItem.right
anchors.top: mainWindow.contentItem.top
Component.onCompleted: {
console.log("MpvVideoItem size:", width, "x", height, "visible:", visible)
}
onWidthChanged: console.log("MpvVideoItem width changed:", width)
onHeightChanged: console.log("MpvVideoItem height changed:", height)
}
WebEngineView
{
id: web
objectName: "web"
width: mainWindow.width
height: mainWindow.height
z: 100
backgroundColor: "transparent"
webChannel: webChannelObject
settings.errorPageEnabled: false
settings.localContentCanAccessRemoteUrls: true
@@ -159,20 +208,17 @@ KonvergoWindow
url: mainWindow.webUrl
focus: true
property string currentHoveredUrl: ""
onLinkHovered: web.currentHoveredUrl = hoveredUrl
width: mainWindow.width
height: mainWindow.height
userScripts: [
WebEngineScript
{
sourceCode: components.system.getNativeShellScript()
injectionPoint: WebEngineScript.DocumentCreation
worldId: WebEngineScript.MainWorld
}
]
onLinkHovered: function(hoveredUrl)
{
web.currentHoveredUrl = hoveredUrl;
}
profile.persistentCookiesPolicy: WebEngineProfile.AllowPersistentCookies
profile.offTheRecord: false
profile.storageName: "JellyfinMediaPlayerStorage"
Component.onCompleted:
{
console.log("WebEngineView size:", width, "x", height, "backgroundColor:", backgroundColor)
forceActiveFocus()
mainWindow.reloadWebClient.connect(reload)
@@ -219,7 +265,7 @@ KonvergoWindow
}
}
onNewViewRequested:
onNewWindowRequested:
{
if (request.userInitiated)
{
@@ -235,7 +281,7 @@ KonvergoWindow
request.accept()
}
onJavaScriptConsoleMessage:
onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID)
{
components.system.jsLog(level, sourceID + ":" + lineNumber + " " + message);
}
@@ -264,14 +310,14 @@ KonvergoWindow
textFormat: Text.StyledText
onLinkActivated:
{
if (link == "reload")
if (url == "reload")
{
errorLabel.visible = false
web.reload()
}
else
{
Qt.openUrlExternally(link)
Qt.openUrlExternally(url)
}
}
}
+6 -6
View File
@@ -8,7 +8,7 @@
/////////////////////////////////////////////////////////////////////////////////////////
bool CachedRegexMatcher::addMatcher(const QString& pattern, const QVariant& result)
{
QRegExp matcher(pattern);
QRegularExpression matcher(pattern);
if (!matcher.isValid())
{
qWarning() << "Could not compile pattern:" << pattern;
@@ -41,22 +41,22 @@ QVariantList CachedRegexMatcher::match(const QString& input)
// otherwise try to iterate our list and find a match
for(const MatcherValuePair& matcher : m_matcherList)
{
QRegExp re(matcher.first);
QRegularExpressionMatch match = matcher.first.match(input);
if (re.indexIn(input) != -1)
if (match.hasMatch())
{
// found match
QVariant returnValue = matcher.second;
if (re.captureCount() > 0 && matcher.second.type() == QVariant::String)
if (match.lastCapturedIndex() > 0 && matcher.second.typeId() == QMetaType::QString)
{
QString value(matcher.second.toString());
for (int i = 0; i < re.captureCount(); i ++)
for (int i = 0; i < match.lastCapturedIndex(); i ++)
{
QString argFmt = QString("%%1").arg(i + 1);
if (value.contains(argFmt))
value = value.arg(re.cap(i + 1));
value = value.arg(match.captured(i + 1));
}
returnValue = QVariant(value);
}
+2 -2
View File
@@ -5,12 +5,12 @@
#ifndef KONVERGO_CACHEDREGEXMATCHER_H
#define KONVERGO_CACHEDREGEXMATCHER_H
#include <QRegExp>
#include <QRegularExpression>
#include <QVariant>
#include <QString>
#include <QHash>
typedef QPair<QRegExp, QVariant> MatcherValuePair;
typedef QPair<QRegularExpression, QVariant> MatcherValuePair;
typedef QList<MatcherValuePair> MatcherValueList;
class CachedRegexMatcher : public QObject
+6 -1
View File
@@ -35,7 +35,12 @@ static void qtMessageOutput(QtMsgType type, const QMessageLogContext& context, c
return;
if (shouldOutputToTerminal) {
QString terminalMsg = msg;
QString terminalMsg;
if (terminalLogLevel == 0) { // debug level
terminalMsg = qFormatLogMessage(type, context, msg);
} else {
terminalMsg = msg;
}
Log::CensorAuthTokens(terminalMsg);
std::cerr << qPrintable(terminalMsg) << std::endl;
}
+19
View File
@@ -0,0 +1,19 @@
//
// Created by Tobias Hieta on 15/05/15.
//
#include "PlatformUtils.h"
#ifdef Q_OS_UNIX
#include <signal.h>
#endif
bool PlatformUtils::isProcessAlive(qint64 pid)
{
#ifdef Q_OS_UNIX
int ret = kill((pid_t)pid, 0);
return ret == 0;
#endif
return false;
}
+18
View File
@@ -0,0 +1,18 @@
//
// Created by Tobias Hieta on 15/05/15.
//
#ifndef KONVERGO_PLATFORMUTILS_H
#define KONVERGO_PLATFORMUTILS_H
#include <QProcess>
class PlatformUtils
{
public:
PlatformUtils() { }
static bool isProcessAlive(qint64 pid);
};
#endif //KONVERGO_PLATFORMUTILS_H
+3 -2
View File
@@ -13,6 +13,7 @@
#include <QFile>
#include <QSaveFile>
#include <QDebug>
#include <QRegularExpression>
#include <mutex>
@@ -57,7 +58,7 @@ QJsonDocument Utils::OpenJsonDocument(const QString& path, QJsonParseError* err)
{
QFile fp(path);
QByteArray fdata;
QRegExp commentMatch("^\\s*//");
QRegularExpression commentMatch("^\\s*//");
if (fp.open(QFile::ReadOnly))
{
@@ -69,7 +70,7 @@ QJsonDocument Utils::OpenJsonDocument(const QString& path, QJsonParseError* err)
break;
// filter all comments
if (commentMatch.indexIn(row) != -1)
if (commentMatch.match(row).hasMatch())
continue;
fdata.append(row);
+19 -7
View File
@@ -2,8 +2,10 @@ cmake_minimum_required(VERSION 3.5.0)
set(CMAKE_AUTOMOC ON)
# Find Qt5 Test module
find_package(Qt5 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS Test Core Network)
# Find Qt6 Test module
find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS
Test Core Network Widgets WebChannel Qml Quick Xml
WebEngineQuick WebEngineCore OpenGL DBus)
# Get all source files that are part of the main application
get_property(ALL_SRCS GLOBAL PROPERTY SRCS_LIST)
@@ -14,13 +16,23 @@ add_executable(test_systemcomponent
${ALL_SRCS}
)
# Link Qt5 libraries and other dependencies
# Link Qt6 libraries and other dependencies
target_link_libraries(test_systemcomponent
shared
Qt5::Test
Qt5::Core
Qt5::Network
${QT5_LIBRARIES}
Qt6::Test
Qt6::Core
Qt6::Network
Qt6::Widgets
Qt6::WebChannel
Qt6::Qml
Qt6::Quick
Qt6::Xml
Qt6::WebEngineQuick
Qt6::WebEngineCore
Qt6::OpenGL
Qt6::DBus
MpvQt::MpvQt
${QT6_LIBRARIES}
${MPV_LIBRARY}
${OPENGL_LIBS}
${OS_LIBS}