mirror of
https://github.com/jellyfin/jellyfin-desktop.git
synced 2026-10-26 16:45:24 +00:00
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:
1 parent
713ac65fab
commit
c7e9cf7486
50 files changed
+1594
-850
No files matched your search
+13
-13
@@ -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
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
|
||||
Vendored
+6
@@ -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)
|
||||
|
||||
Vendored
+7
-7
@@ -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.
|
||||
Vendored
+4
@@ -7,3 +7,7 @@ add_library(plistparser STATIC
|
||||
plistserializer.h
|
||||
)
|
||||
std_target_properties(plistparser)
|
||||
target_link_libraries(plistparser
|
||||
Qt6::Core
|
||||
Qt6::Xml
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -9,6 +9,7 @@
|
||||
#ifndef _DISPLAYMANAGER_H_
|
||||
#define _DISPLAYMANAGER_H_
|
||||
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
#include <QPoint>
|
||||
#include <QString>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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,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)
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include <QtDBus/QDBusConnection>
|
||||
#include <QtDBus/QDBusInterface>
|
||||
#include <QtDBus/QDBusReply>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusReply>
|
||||
|
||||
#include "PowerComponentDBus.h"
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
@@ -6,6 +6,7 @@
|
||||
#define KONVERGO_LOCALJSONCLIENT_H
|
||||
|
||||
#include <QLocalSocket>
|
||||
#include <QVariant>
|
||||
|
||||
class LocalJsonClient : public QLocalSocket
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user