diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e48093db..83c75eafe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index c62c0530c..847824fc0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/CMakeModules/DependencyConfiguration.cmake b/CMakeModules/DependencyConfiguration.cmake index 026a4bffc..545c13e52 100644 --- a/CMakeModules/DependencyConfiguration.cmake +++ b/CMakeModules/DependencyConfiguration.cmake @@ -41,3 +41,4 @@ endif(IS_DIRECTORY ${DEPENDENCY_ROOT}) find_package(Threads REQUIRED) find_package(PythonInterp REQUIRED) +find_package(MpvQt REQUIRED) diff --git a/CMakeModules/QtConfiguration.cmake b/CMakeModules/QtConfiguration.cmake index 66e791983..810b51a69 100644 --- a/CMakeModules/QtConfiguration.cmake +++ b/CMakeModules/QtConfiguration.cmake @@ -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() diff --git a/README.md b/README.md index 1384c9bbb..0d3b3540c 100644 --- a/README.md +++ b/README.md @@ -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 with your QT installation location): diff --git a/debian/changelog b/debian/changelog index e998d1798..bb4f8c0b2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +jellyfin-media-player (1.13.0-1) unstable; urgency=medium + + * Update to Qt 6 + + -- Izzie Walton 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) diff --git a/debian/control b/debian/control index 5d1b8d09c..a49b2661e 100644 --- a/debian/control +++ b/debian/control @@ -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. diff --git a/external/plistparser/CMakeLists.txt b/external/plistparser/CMakeLists.txt index 871679a67..e12818cb0 100644 --- a/external/plistparser/CMakeLists.txt +++ b/external/plistparser/CMakeLists.txt @@ -7,3 +7,7 @@ add_library(plistparser STATIC plistserializer.h ) std_target_properties(plistparser) +target_link_libraries(plistparser +Qt6::Core +Qt6::Xml +) diff --git a/for-web-developers.md b/for-web-developers.md index 474195467..00b57307b 100644 --- a/for-web-developers.md +++ b/for-web-developers.md @@ -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: diff --git a/native/jmpInputPlugin.js b/native/jmpInputPlugin.js index dd0421087..bb61e0bed 100644 --- a/native/jmpInputPlugin.js +++ b/native/jmpInputPlugin.js @@ -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"); })(); } diff --git a/native/mpvVideoPlayer.js b/native/mpvVideoPlayer.js index 14201c184..ec5dd479b 100644 --- a/native/mpvVideoPlayer.js +++ b/native/mpvVideoPlayer.js @@ -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()); + // Hide backdrop when playback starts + const dlg = this._videoDialog; + if (dlg) { + dlg.style.backgroundImage = ''; + } + + // Navigate to OSD view to show playback screen if (this._currentPlayOptions.fullscreen) { - this.appRouter.showVideoOsd().then(this.onNavigatedToOsd); - } else { - this.setTransparency('backdrop'); - this._videoDialog.dlg.style.zIndex = 'unset'; + this.appRouter.showVideoOsd(); + // Lower video dialog z-index so OSD can receive input + if (dlg) { + dlg.style.zIndex = 'unset'; + } } - // Need to override default style. - this._videoDialog.style.setProperty('background', 'transparent', 'important'); + // 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,105 +248,44 @@ return this.appSettings.get('volume') || 1; } - /** - * @private - */ - getRelativeIndexByType(mediaStreams, jellyIndex, streamType) { - let relIndex = 1; - for (const source of mediaStreams) { - if (source.Type != streamType || source.IsExternal) { - continue; - } - if (source.Index == jellyIndex) { - return relIndex; + 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; + } } - - relIndex += 1; } - - return null; } /** * @private */ getStreamByIndex(mediaStreams, jellyIndex) { - for (const source of mediaStreams) { - if (source.Index == jellyIndex) { - return source; + for (const stream of mediaStreams) { + if (stream.Index == jellyIndex) { + return stream; } } - 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'; + getRelativeIndexByType(mediaStreams, jellyIndex, streamType) { + let relIndex = 1; + for (const source of mediaStreams) { + if (source.Type != streamType || source.IsExternal) { + continue; } - } - - 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; - } + if (source.Index == jellyIndex) { + return relIndex; } + relIndex += 1; } + return null; } /** @@ -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 */ diff --git a/resources/misc/licenses.txt b/resources/misc/licenses.txt index 0f6f5dd7a..34fa07de7 100644 --- a/resources/misc/licenses.txt +++ b/resources/misc/licenses.txt @@ -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) diff --git a/scripts/install-dylibs.py b/scripts/install-dylibs.py index 577f62e78..1fd71048e 100755 --- a/scripts/install-dylibs.py +++ b/scripts/install-dylibs.py @@ -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) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 579e2fb05..39cf4ed5f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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") diff --git a/src/core/ComponentManager.cpp b/src/core/ComponentManager.cpp index 46a291e07..332bf8c97 100644 --- a/src/core/ComponentManager.cpp +++ b/src/core/ComponentManager.cpp @@ -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()); diff --git a/src/core/Globals.cpp b/src/core/Globals.cpp index 0d672e8d0..c7607a258 100644 --- a/src/core/Globals.cpp +++ b/src/core/Globals.cpp @@ -3,9 +3,9 @@ // #include "Globals.h" -#include "ui/KonvergoWindow.h" #include +#include 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(rootObject); - Q_ASSERT_X(g_qmlEngine, "Globals", "RootObject in QmlEngine is not a KonvergoWindow"); + auto window = qobject_cast(rootObject); + Q_ASSERT_X(g_qmlEngine, "Globals", "RootObject in QmlEngine is not a QQuickWindow"); return window; } diff --git a/src/core/Globals.h b/src/core/Globals.h index 06f9ba6ff..c80adf377 100644 --- a/src/core/Globals.h +++ b/src/core/Globals.h @@ -2,8 +2,7 @@ #define KONVERGOENGINE_H #include - -class KonvergoWindow; +#include ///////////////////////////////////////////////////////////////////////////////////////// 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 diff --git a/src/display/DisplayManager.h b/src/display/DisplayManager.h index 47a83f4fb..4031cc0c2 100644 --- a/src/display/DisplayManager.h +++ b/src/display/DisplayManager.h @@ -9,6 +9,7 @@ #ifndef _DISPLAYMANAGER_H_ #define _DISPLAYMANAGER_H_ +#include #include #include #include diff --git a/src/display/rpi/DisplayManagerRPI.cpp b/src/display/rpi/DisplayManagerRPI.cpp index cc5e83055..85740ad52 100644 --- a/src/display/rpi/DisplayManagerRPI.cpp +++ b/src/display/rpi/DisplayManagerRPI.cpp @@ -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(); diff --git a/src/input/InputComponent.cpp b/src/input/InputComponent.cpp index 0f5f12905..05b82bfee 100644 --- a/src/input/InputComponent.cpp +++ b/src/input/InputComponent.cpp @@ -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()); } diff --git a/src/main.cpp b/src/main.cpp index 09a5c4cc2..343de4abd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,24 +5,29 @@ #include #include #include -#include -#include +#include +#include +#include +#include #include +#include #include #include #include #include "shared/Names.h" #include "system/SystemComponent.h" +#include #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 +81,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(contents.size()), contents.data()); } ///////////////////////////////////////////////////////////////////////////////////////// @@ -139,6 +148,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 +189,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 +231,7 @@ int main(int argc, char *argv[]) webEngineDataDir = d.absolutePath() + "/QtWebEngine"; } + QtWebEngineQuick::initialize(); QApplication app(newArgc, newArgv); #if defined(Q_OS_WIN) @@ -262,8 +280,6 @@ int main(int argc, char *argv[]) SettingsComponent::Get().setCommandLineValues(parser.optionNames()); - QtWebEngine::initialize(); - // Configure QtWebEngine paths QWebEngineProfile* defaultProfile = QWebEngineProfile::defaultProfile(); defaultProfile->setCachePath(webEngineDataDir); @@ -272,27 +288,84 @@ int main(int argc, char *argv[]) // load QtWebChannel so that we can register our components with it. QQmlApplicationEngine *engine = Globals::Engine(); - KonvergoWindow::RegisterClass(); Globals::SetContextProperty("components", &ComponentManager::Get().getQmlPropertyMap()); // the only way to detect if QML parsing fails is to hook to this signal and then see // 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(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(window->property("webChannel")); Q_ASSERT(webChannel); ComponentManager::Get().setWebChannel(qobject_cast(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"))); diff --git a/src/player/CMakeLists.txt b/src/player/CMakeLists.txt index c75f49746..e8f12773a 100644 --- a/src/player/CMakeLists.txt +++ b/src/player/CMakeLists.txt @@ -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) diff --git a/src/player/MpvVideoItem.cpp b/src/player/MpvVideoItem.cpp new file mode 100644 index 000000000..0c7e9c4f9 --- /dev/null +++ b/src/player/MpvVideoItem.cpp @@ -0,0 +1,39 @@ +#include "MpvVideoItem.h" +#include "PlayerComponent.h" +#include +#include + +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"; + } +} diff --git a/src/player/MpvVideoItem.h b/src/player/MpvVideoItem.h new file mode 100644 index 000000000..6e086a1f5 --- /dev/null +++ b/src/player/MpvVideoItem.h @@ -0,0 +1,23 @@ +#ifndef MPVVIDEOITEM_H +#define MPVVIDEOITEM_H + +#include + +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 diff --git a/src/player/OpenGLDetect.cpp b/src/player/OpenGLDetect.cpp index 6212e7c66..1312dd44f 100644 --- a/src/player/OpenGLDetect.cpp +++ b/src/player/OpenGLDetect.cpp @@ -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); diff --git a/src/player/PlayerComponent.cpp b/src/player/PlayerComponent.cpp index 796c1df44..e1686a048 100644 --- a/src/player/PlayerComponent.cpp +++ b/src/player/PlayerComponent.cpp @@ -13,12 +13,14 @@ #include "ComponentManager.h" #include "settings/SettingsSection.h" -#include "PlayerQuickItem.h" +#include "MpvVideoItem.h" #include "input/InputComponent.h" +#include #include #include #include +#include #if !defined(Q_OS_WIN) #include @@ -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("Konvergo", 1, 0, "MpvVideo"); // deprecated name - qmlRegisterType("Konvergo", 1, 0, "KonvergoVideo"); + qmlRegisterType("Konvergo", 1, 0, "MpvVideo"); // deprecated name + qmlRegisterType("Konvergo", 1, 0, "KonvergoVideo"); + qmlRegisterType("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 + + mpv_request_log_messages(m_mpv->mpv(), "terminal-default"); + m_mpv->setProperty("msg-level", "all=v"); - // 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_set_wakeup_callback(m_mpv->mpv(), wakeup_cb, this); - mpv_set_wakeup_callback(m_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("video"); - if (!video) + qDebug() << "PlayerComponent::setQtQuickWindow called"; + MpvVideoItem* video = window->findChild("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(); + + // Handle integer Jellyfin stream index (new format) + if (!isStringFormat && streamSelection.canConvert()) { + 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()) 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()) return time.toDouble(); return 0; } @@ -994,7 +1099,7 @@ void PlayerComponent::updateAudioDeviceList() QSet 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 PlayerComponent::installedCodecDrivers() { QList 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& 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"; diff --git a/src/player/PlayerComponent.h b/src/player/PlayerComponent.h index a0f7451e3..79670638d 100644 --- a/src/player/PlayerComponent.h +++ b/src/player/PlayerComponent.h @@ -17,6 +17,7 @@ #include +class MpvController; /////////////////////////////////////////////////////////////////////////////////////////////////// class PlayerComponent : public ComponentBase @@ -34,14 +35,14 @@ class PlayerComponent : public ComponentBase ~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 @@ class PlayerComponent : public ComponentBase // 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 @@ class PlayerComponent : public ComponentBase 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 Q_SLOTS: void startCodecsLoading(std::function 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 Q_SLOTS: bool m_doAc3Transcoding; QStringList m_passthroughCodecs; QVariantMap m_serverMediaInfo; - QString m_currentSubtitleStream; - QString m_currentAudioStream; + QVariant m_currentSubtitleStream; + QVariant m_currentAudioStream; QRect m_videoRectangle; }; diff --git a/src/player/PlayerQuickItem.cpp b/src/player/PlayerQuickItem.cpp deleted file mode 100644 index 1d18206fe..000000000 --- a/src/player/PlayerQuickItem.cpp +++ /dev/null @@ -1,338 +0,0 @@ -#include "PlayerQuickItem.h" - -#include - -#include -#include -#include -#include - -#include - -#include -#include - -#include - -#include "utils/Utils.h" - - -#if defined(Q_OS_WIN32) -#include -#include -#include -#endif - -#ifdef USE_X11EXTRAS -#include -#include -#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(); -} diff --git a/src/player/PlayerQuickItem.h b/src/player/PlayerQuickItem.h deleted file mode 100644 index 644a62e0f..000000000 --- a/src/player/PlayerQuickItem.h +++ /dev/null @@ -1,72 +0,0 @@ -#ifndef PLAYERQUICKITEM_H -#define PLAYERQUICKITEM_H - -#include -#include -#include - -#include -#include - -#ifdef Q_OS_WIN32 -#include -#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 diff --git a/src/player/QtHelper.h b/src/player/QtHelper.h index 0f6649060..2bb36f690 100644 --- a/src/player/QtHelper.h +++ b/src/player/QtHelper.h @@ -136,11 +136,7 @@ struct node_builder { 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(v.type()) == static_cast(t); + return v.typeId() == t; } void set(mpv_node *dst, const QVariant &src) { if (test_type(src, QMetaType::QString)) { diff --git a/src/power/PowerComponentDBus.cpp b/src/power/PowerComponentDBus.cpp index db6e2c1d3..d0af38493 100644 --- a/src/power/PowerComponentDBus.cpp +++ b/src/power/PowerComponentDBus.cpp @@ -1,6 +1,6 @@ -#include -#include -#include +#include +#include +#include #include "PowerComponentDBus.h" diff --git a/src/settings/SettingsComponent.cpp b/src/settings/SettingsComponent.cpp index 6922b7f35..93411f833 100644 --- a/src/settings/SettingsComponent.cpp +++ b/src/settings/SettingsComponent.cpp @@ -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); } diff --git a/src/settings/SettingsSection.cpp b/src/settings/SettingsSection.cpp index e4dcff322..542628e35 100644 --- a/src/settings/SettingsSection.cpp +++ b/src/settings/SettingsSection.cpp @@ -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) { diff --git a/src/settings/SettingsSection.h b/src/settings/SettingsSection.h index 8c04df839..bebe913ae 100644 --- a/src/settings/SettingsSection.h +++ b/src/settings/SettingsSection.h @@ -20,6 +20,7 @@ class SettingsSection : public QObject 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); diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt index a268db888..a06617bf7 100644 --- a/src/shared/CMakeLists.txt +++ b/src/shared/CMakeLists.txt @@ -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) diff --git a/src/shared/LocalJsonClient.h b/src/shared/LocalJsonClient.h index d5e8c50bc..d74099a0a 100644 --- a/src/shared/LocalJsonClient.h +++ b/src/shared/LocalJsonClient.h @@ -6,6 +6,7 @@ #define KONVERGO_LOCALJSONCLIENT_H #include +#include class LocalJsonClient : public QLocalSocket { diff --git a/src/system/SystemComponent.cpp b/src/system/SystemComponent.cpp index 3d6407960..effaeb3a4 100644 --- a/src/system/SystemComponent.cpp +++ b/src/system/SystemComponent.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -24,7 +25,6 @@ #include "SystemComponent.h" #include "Version.h" #include "settings/SettingsComponent.h" -#include "ui/KonvergoWindow.h" #include "settings/SettingsSection.h" #include "Paths.h" #include "Names.h" @@ -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)); diff --git a/src/ui/EventFilter.cpp b/src/ui/EventFilter.cpp index 1d01da95f..042e99e12 100644 --- a/src/ui/EventFilter.cpp +++ b/src/ui/EventFilter.cpp @@ -6,7 +6,7 @@ #include "system/SystemComponent.h" #include "settings/SettingsComponent.h" #include "input/InputKeyboard.h" -#include "KonvergoWindow.h" +#include #include #include @@ -64,7 +64,7 @@ static QString keyEventToKeyString(QKeyEvent *kevent) /////////////////////////////////////////////////////////////////////////////////////////////////// bool EventFilter::eventFilter(QObject* watched, QEvent* event) { - KonvergoWindow* window = qobject_cast(parent()); + QQuickWindow* window = qobject_cast(parent()); if (window && window->property("webDesktopMode").toBool()) { diff --git a/src/ui/KonvergoWindow.cpp b/src/ui/KonvergoWindow.cpp deleted file mode 100644 index 949371c02..000000000 --- a/src/ui/KonvergoWindow.cpp +++ /dev/null @@ -1,723 +0,0 @@ -#include "KonvergoWindow.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/Version.h" -#include "input/InputKeyboard.h" -#include "settings/SettingsComponent.h" -#include "settings/SettingsSection.h" -#include "system/SystemComponent.h" -#include "player/PlayerComponent.h" -#include "player/PlayerQuickItem.h" -#include "display/DisplayComponent.h" -#include "taskbar/TaskbarComponent.h" -#include "utils/Utils.h" -#include "Globals.h" -#include "EventFilter.h" - -#ifdef USE_X11EXTRAS -#include -#include -#endif - -/////////////////////////////////////////////////////////////////////////////////////////////////// -class ScopedDecrementer -{ - Q_DISABLE_COPY(ScopedDecrementer) - - int* m_value; - -public: - ScopedDecrementer(int* value) : m_value(value) {} - ~ScopedDecrementer() { (*m_value)--; } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////// -KonvergoWindow::KonvergoWindow(QWindow* parent) : - QQuickWindow(parent), - m_debugLayer(false), - m_ignoreFullscreenSettingsChange(0), - m_showedUpdateDialog(false), - m_osxPresentationOptions(0) -{ - // NSWindowCollectionBehaviorFullScreenPrimary is only set on OSX if Qt::WindowFullscreenButtonHint is set on the window. - setFlags(flags() | Qt::WindowFullscreenButtonHint); - - m_infoTimer = new QTimer(this); - m_infoTimer->setInterval(1000); - m_webDesktopMode = (SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "webMode").toString() == "desktop"); - - installEventFilter(new EventFilter(this)); - - connect(m_infoTimer, &QTimer::timeout, this, &KonvergoWindow::updateDebugInfo); - - InputComponent::Get().registerHostCommand("close", this, "close"); - InputComponent::Get().registerHostCommand("toggleDebug", this, "toggleDebug"); - InputComponent::Get().registerHostCommand("reload", this, "reloadWeb"); - InputComponent::Get().registerHostCommand("fullscreen", this, "toggleFullscreen"); - InputComponent::Get().registerHostCommand("minimize", this, "minimizeWindow"); - InputComponent::Get().registerHostCommand("switchMode", this, "toggleWebMode"); - -#ifdef TARGET_RPI - // On RPI, we use dispmanx layering - the video is on a layer below Konvergo, - // and during playback the Konvergo window is partially transparent. The OSD - // will be visible on top of the video as part of the Konvergo window. - setColor(QColor("transparent")); -#else - setColor(QColor("#000000")); -#endif - -#ifdef USE_X11EXTRAS - // On Gnome show a darker title bar - if (QX11Info::isPlatformX11()) - { - Display* dpy = QX11Info::display(); - if (dpy) - { - WId win = winId(); - XChangeProperty(dpy, win, - XInternAtom(dpy, "_GTK_THEME_VARIANT", false), - XInternAtom(dpy, "UTF8_STRING", false), - 8, PropModeReplace, (unsigned char *) "dark", 4); - } - } -#endif - - loadGeometry(); - - connect(SettingsComponent::Get().getSection(SETTINGS_SECTION_MAIN), &SettingsSection::valuesUpdated, - this, &KonvergoWindow::updateMainSectionSettings); - - connect(this, &KonvergoWindow::visibilityChanged, - this, &KonvergoWindow::onVisibilityChanged); - - connect(this, &KonvergoWindow::screenChanged, - this, &KonvergoWindow::updateCurrentScreen, Qt::QueuedConnection); - connect(this, &KonvergoWindow::xChanged, - this, &KonvergoWindow::updateCurrentScreen, Qt::QueuedConnection); - connect(this, &KonvergoWindow::yChanged, - this, &KonvergoWindow::updateCurrentScreen, Qt::QueuedConnection); - connect(this, &KonvergoWindow::visibilityChanged, - this, &KonvergoWindow::updateCurrentScreen, Qt::QueuedConnection); - connect(this, &KonvergoWindow::windowStateChanged, - this, &KonvergoWindow::updateCurrentScreen, Qt::QueuedConnection); - - connect(this, &KonvergoWindow::enableVideoWindowSignal, - this, &KonvergoWindow::enableVideoWindow, Qt::QueuedConnection); - - connect(&PlayerComponent::Get(), &PlayerComponent::windowVisible, - this, &KonvergoWindow::playerWindowVisible, Qt::QueuedConnection); - - // 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); - -#ifdef Q_OS_MAC - m_osxPresentationOptions = 0; -#endif - -#ifdef KONVERGO_OPENELEC - setVisibility(QWindow::FullScreen); -#else - updateWindowState(false); -#endif - - updateScreens(); - - connect(qApp, &QGuiApplication::screenAdded, this, &KonvergoWindow::onScreenAdded); - connect(qApp, &QGuiApplication::screenRemoved, this, &KonvergoWindow::onScreenRemoved); - - emit enableVideoWindowSignal(); -} - -///////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::closingWindow() -{ - if (!SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "fullscreen").toBool()) - saveGeometry(); - - qApp->quit(); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -KonvergoWindow::~KonvergoWindow() -{ - DisplayComponent::Get().setApplicationWindow(nullptr); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -bool KonvergoWindow::fitsInScreens(const QRect& rc) -{ - for(QScreen *screen : QGuiApplication::screens()) - { - if (screen->virtualGeometry().isValid() && screen->virtualGeometry().contains(rc)) - return true; - } - return false; -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::saveGeometry() -{ - qDebug() << "Window state when saving geometry:" << visibility(); - - QRect rc = geometry(); - - // lets make sure we are not saving something craycray - if (rc.size().width() < windowMinSize().width() || rc.size().height() < windowMinSize().height()) - return; - - if (!fitsInScreens(rc)) - return; - - qDebug() << "Saving window geometry:" << rc; - - if (visibility() != QWindow::Hidden) - { - QVariantMap map = {{"x", rc.x()}, {"y", rc.y()}, - {"width", rc.width()}, {"height", rc.height()}}; - SettingsComponent::Get().setValue(SETTINGS_SECTION_STATE, "geometry", map); - SettingsComponent::Get().setValue(SETTINGS_SECTION_STATE, "maximized", visibility() == QWindow::Maximized); - } - QScreen *curScreen = screen(); - SettingsComponent::Get().setValue(SETTINGS_SECTION_STATE, "lastUsedScreen", curScreen ? curScreen->name() : ""); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -QRect KonvergoWindow::loadGeometry() -{ - QRect rc = loadGeometryRect(); - QScreen* myScreen = loadLastScreen(); - if (!myScreen) - myScreen = screen(); - - QRect nsize = rc; - - if (SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "fullscreen").toBool()) - { - qDebug() << "Load FullScreen geo..."; - - if (myScreen) - { - // On OSX we need to set the geometry to the size we want when we - // return from fullscreen otherwise when we exit fullscreen it - // will stay small or big. On Windows we need to set it to max - // resolution for the screen (i.e. fullscreen) otherwise it will - // just scale the webcontent to the minimum size we have defined - // - #ifndef Q_OS_MAC - nsize = myScreen->geometry(); - #endif - setGeometry(nsize); - - setScreen(myScreen); - } - } - else - { - setGeometry(nsize); - if (SettingsComponent::Get().value(SETTINGS_SECTION_STATE, "maximized").toBool()) - setVisibility(QWindow::Maximized); - saveGeometry(); - } - - return nsize; -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -QRect KonvergoWindow::loadGeometryRect() -{ - // if we dont have anything, default to 720p in the middle of the screen - QScreen *curScreen = screen(); - QRect defaultRect = QRect(0, 0, WEBUI_SIZE.width(), WEBUI_SIZE.height()); - if (curScreen) - { - defaultRect = QRect((curScreen->geometry().width() - WEBUI_SIZE.width()) / 2, - (curScreen->geometry().height() - WEBUI_SIZE.height()) / 2, - WEBUI_SIZE.width(), WEBUI_SIZE.height()); - } - - QVariantMap map = SettingsComponent::Get().value(SETTINGS_SECTION_STATE, "geometry").toMap(); - if (map.isEmpty()) - return defaultRect; - - QRect rc(map["x"].toInt(), map["y"].toInt(), map["width"].toInt(), map["height"].toInt()); - - qDebug() << "Restoring geo:" << rc; - - if (!rc.isValid() || rc.isEmpty()) - { - qDebug() << "Geo bad, going for defaults"; - return defaultRect; - } - - QSize minsz = windowMinSize(); - - // Clamp to min size if we have really small values in there - if (rc.size().width() < minsz.width()) - rc.setWidth(minsz.width()); - if (rc.size().height() < minsz.height()) - rc.setHeight(minsz.height()); - - // also make sure we are not putting windows outside the screen somewhere - if (!fitsInScreens(rc)) - { - qDebug() << "Could not fit stored geo into current screens"; - return defaultRect; - } - - return rc; -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::enableVideoWindow() -{ - PlayerComponent::Get().setWindow(this); - DisplayComponent::Get().setApplicationWindow(this); - TaskbarComponent::Get().setWindow(this); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::setFullScreen(bool enable) -{ - qDebug() << "setting fullscreen = " << enable; - - SettingsComponent::Get().setValue(SETTINGS_SECTION_MAIN, "fullscreen", enable); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::toggleWebMode() -{ - if (!m_webDesktopMode) - SettingsComponent::Get().setValue(SETTINGS_SECTION_MAIN, "webMode", "desktop"); - else - SettingsComponent::Get().setValue(SETTINGS_SECTION_MAIN, "webMode", "tv"); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::setAlwaysOnTop(bool enable) -{ - qDebug() << "setting always on top = " << enable; - - // Update the settings value. - SettingsComponent::Get().setValue(SETTINGS_SECTION_MAIN, "alwaysOnTop", enable); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::playerWindowVisible(bool visible) -{ - // adjust webengineview transparecy depending on player visibility - QQuickItem *web = findChild("web"); - if (web) - web->setProperty("backgroundColor", visible ? "transparent" : "#000000"); - -#ifdef Q_OS_MAC - // On OSX, initializing VideoTooolbox (hardware decoder API) will mysteriously - // show the hidden mouse pointer again. The VTDecompressionSessionCreate API - // function does this, and we have no influence over its behavior. - if (visible && !SystemComponent::Get().cursorVisible()) - { - // "Refresh" it. (There doesn't seem to be a nicer way, and we have to do - // this on the Cocoa level too.) - SystemComponent::Get().setCursorVisibility(true); - SystemComponent::Get().setCursorVisibility(false); - } -#endif -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::updateMainSectionSettings(const QVariantMap& values) -{ - // update mouse visibility if needed - if (values.find("disablemouse") != values.end()) - { - SystemComponent::Get().setCursorVisibility(!SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "disablemouse").toBool()); - } - - if (values.contains("alwaysOnTop")) - updateWindowState(); - - if (values.contains("fullscreen") && !m_ignoreFullscreenSettingsChange) - updateWindowState(); - - if (values.contains("webMode")) - { - InputComponent::Get().cancelAutoRepeat(); - bool oldDesktopMode = m_webDesktopMode; - bool newDesktopMode = (SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "webMode").toString() == "desktop"); - - if (SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "layout").toString() != "auto") - { - if (oldDesktopMode != newDesktopMode) - { - PlayerComponent::Get().stop(); - m_webDesktopMode = newDesktopMode; - emit webDesktopModeChanged(); - emit webUrlChanged(); - } - } - else - { - if (oldDesktopMode != newDesktopMode) - { - QTimer::singleShot(0, [this, newDesktopMode] - { - PlayerComponent::Get().stop(); - m_webDesktopMode = newDesktopMode; - emit webDesktopModeChanged(); - emit webUrlChanged(); - - if (m_webDesktopMode) - SystemComponent::Get().setCursorVisibility(true); - }); - } - } - } - - if (values.contains("startupurl")) - emit webUrlChanged(); - - if (values.contains("forceFSScreen")) - updateForcedScreen(); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::updateForcedScreen() -{ - QString screenName = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "forceFSScreen").toString(); - - if (screenName.isEmpty()) - return; - - for (QScreen* scr : QGuiApplication::screens()) - { - if (scr->name() == screenName) - { - qDebug() << "Forcing screen to" << scr->name(); - setScreen(scr); - setGeometry(scr->geometry()); - setVisibility(QWindow::FullScreen); - InputComponent::Get().cancelAutoRepeat(); - return; - } - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::updateWindowState(bool saveGeo) -{ - if (SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "fullscreen").toBool() || SystemComponent::Get().isOpenELEC() || SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "forceAlwaysFS").toBool()) - { - // if we were go from windowed to fullscreen - // we want to store our current windowed position - if (!isFullScreen() && saveGeo) - saveGeometry(); - - setVisibility(QWindow::FullScreen); - - // When fullscreening explicitly, we might have to move the window to a - // different screen, as Qt will fullscreen to the current screen. - QTimer::singleShot(200, [=] - { - updateForcedScreen(); - }); - } - else - { - setVisibility(QWindow::Windowed); - loadGeometry(); - - Qt::WindowFlags forceOnTopFlags = Qt::WindowStaysOnTopHint; -#ifdef Q_WS_X11 - forceOnTopFlags = forceOnTopFlags | Qt::X11BypassWindowManagerHint; -#endif - - if (SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "alwaysOnTop").toBool()) - setFlags(flags() | forceOnTopFlags); - else - setFlags(flags() &~ forceOnTopFlags); - } - - InputComponent::Get().cancelAutoRepeat(); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -QScreen* KonvergoWindow::findCurrentScreen() -{ - // Return the screen that contains most of the window. Quite possible that - // screen() would be sufficient, at least once the Qt bug returning a wrong - // QScreen on Windows is fixed. - QScreen *best = nullptr; - qint64 bestArea = 0; - for(QScreen* screen : qApp->screens()) - { - QRect areaRC = screen->geometry().intersected(geometry()); - qint64 area = areaRC.width() * (qint64)areaRC.height(); - if (!best || area > bestArea) - { - best = screen; - bestArea = area; - } - } - return best ? best : screen(); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::onVisibilityChanged(QWindow::Visibility visibility) -{ - qDebug() << "QWindow visibility set to" << visibility; - - if (visibility == QWindow::Windowed && SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "forceAlwaysFS").toBool()) - { - qWarning() << "Forcing re-entering fullscreen because of forceAlwaysFS setting!"; - updateForcedScreen(); // if a specific screen is forced, try to move the window there - setVisibility(QWindow::FullScreen); - return; - } - - if (visibility == QWindow::FullScreen || visibility == QWindow::Windowed) - { - m_ignoreFullscreenSettingsChange++; - ScopedDecrementer decrement(&m_ignoreFullscreenSettingsChange); - - bool fs = visibility == QWindow::FullScreen; - SettingsComponent::Get().setValue(SETTINGS_SECTION_MAIN, "fullscreen", fs); - - SystemComponent::Get().setCursorVisibility(false); - } - - InputComponent::Get().cancelAutoRepeat(); -} - -///////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::focusOutEvent(QFocusEvent * ev) -{ -#ifdef Q_OS_WIN32 - // Do this to workaround DWM compositor bugs with fullscreened OpenGL applications. - // The compositor will not properly redraw anything when focusing other windows. - if (visibility() == QWindow::FullScreen && SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "minimizeOnDefocus").toBool()) - { - qDebug() << "minimizing window"; - showMinimized(); - } -#endif - - QQuickWindow::focusOutEvent(ev); -} - -///////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::keyPressEvent(QKeyEvent *ev) -{ - if (ev->key() == Qt::Key_Escape && isFullScreen()) - { - setFullScreen(false); - return; - } - - QQuickWindow::keyPressEvent(ev); -} - -///////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::RegisterClass() -{ - qmlRegisterType("Konvergo", 1, 0, "KonvergoWindow"); -} - -///////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::updateDebugInfo() -{ - if (m_systemDebugInfo.size() == 0) - m_systemDebugInfo = SystemComponent::Get().debugInformation(); - m_debugInfo = m_systemDebugInfo; - m_debugInfo += DisplayComponent::Get().debugInformation(); - PlayerQuickItem* video = findChild("video"); - if (video) - m_debugInfo += video->debugInfo(); - QString infoString; - QDebug info(&infoString); - info << "Qt windowing info:\n"; - info << " FS: " << visibility() << "\n"; - info << " Geo: " << geometry() << "\n"; - for (QScreen* scr : QGuiApplication::screens()) - { - info << " Screen" << scr->name() << scr->geometry() << "\n"; - } - -#ifdef Q_OS_WIN32 - HMONITOR mon = MonitorFromWindow((HWND)winId(), MONITOR_DEFAULTTONEAREST); - MONITORINFO moninfo = {}; - moninfo.cbSize = sizeof(moninfo); - RECT winrc; - if (GetMonitorInfo(mon, &moninfo) &&GetWindowRect((HWND)winId(), &winrc)) - { - RECT rc = moninfo.rcMonitor; - info << " Win32 window" << QString("%1/%2 %3x%4").arg(rc.left).arg(rc.top).arg(rc.right).arg(rc.bottom) << QString("%1/%2 %3x%4").arg(winrc.left).arg(winrc.top).arg(winrc.right).arg(winrc.bottom) << "\n"; - } -#endif - - info << "\n"; - m_debugInfo += infoString; - m_videoInfo = PlayerComponent::Get().videoInformation(); - emit debugInfoChanged(); -} - -///////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::toggleDebug() -{ - if (property("showDebugLayer").toBool()) - { - m_infoTimer->stop(); - setProperty("showDebugLayer", false); - } - else - { - m_infoTimer->start(); - updateDebugInfo(); - setProperty("showDebugLayer", true); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::resizeEvent(QResizeEvent* event) -{ - qDebug() << "resize event:" << event->size(); - - // This next block was added at some point to workaround a problem with - // resizing on windows. Unfortunately it broke the desktop client behavior - // and when retried on Windows 10 with Qt5.7 the original bug seems to be - // gone. I'll keep this code around until such a time that we dont get any - // complaints about it. - // - #if 0 - // This next block should never really be needed in a prefect world... - // Unfortunatly this is an imperfect world and on windows sometimes what - // would happen on startup is that we got a resize event that would make - // the window much smaller than fullscreen. - // - - if (isFullScreen()) - { - QSize fsSize = screen()->size(); - if (event->size().width() < fsSize.width() || event->size().height() < fsSize.height()) - { - qDebug() << "Ignoring resize event when in fullscreen..."; - return; - } - } - #endif - - QQuickWindow::resizeEvent(event); -} - -///////////////////////////////////////////////////////////////////////////////////////// -QScreen* KonvergoWindow::loadLastScreen() -{ - QString screenName = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "forceFSScreen").toString(); - if (screenName.isEmpty()) - screenName = SettingsComponent::Get().value(SETTINGS_SECTION_STATE, "lastUsedScreen").toString(); - if (screenName.isEmpty()) - return nullptr; - - for (QScreen* scr : QGuiApplication::screens()) - { - if (scr->name() == screenName) - return scr; - } - - qDebug() << "Tried to find screen:" << screenName << "but it was not present"; - - return nullptr; -} - -///////////////////////////////////////////////////////////////////////////////////////// -QString KonvergoWindow::webUrl() -{ - return SettingsComponent::Get().getWebClientUrl(m_webDesktopMode); -} - -///////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::updateScreens() -{ - QScreen* windowScreen = findCurrentScreen(); - QString screenName = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "forceFSScreen").toString(); - - QVariantList settingList; - - QVariantMap defentry; - defentry["value"] = ""; - defentry["title"] = "Auto"; - - settingList << defentry; - - bool currentPresent = false; - int num = 0; - for(QScreen* screen : qApp->screens()) - { - QRect rc = screen->geometry(); - bool active = screen == windowScreen; - - QVariantMap entry; - entry["value"] = screen->name(); - entry["title"] = - QString("%1,%2 %3x%4").arg(rc.left()).arg(rc.top()).arg(rc.right()).arg(rc.bottom()) + - " (" + screen->name() + ")" + - (active ? " *" : ""); - - settingList << entry; - - bool selected = screen->name() == screenName; - if (selected) - currentPresent = true; - - qDebug() << "Screen" << (num++) << screen << screen->geometry() - << screen->virtualGeometry() << "active:" << active - << "selected:" << selected; - } - - if (!currentPresent && !screenName.isEmpty()) - { - QVariantMap entry; - entry["value"] = screenName; - entry["title"] = "[Disconnected: " + screenName + "]"; - - settingList << entry; - } - - SettingsComponent::Get().updatePossibleValues(SETTINGS_SECTION_MAIN, "forceFSScreen", settingList); - - m_currentScreenName = windowScreen ? windowScreen->name() : ""; -} - -///////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::onScreenAdded(QScreen *screen) -{ - updateScreens(); - // The timer is out of fear for chaotic mid-change states. - QTimer::singleShot(200, [this] - { - updateForcedScreen(); - }); -} - -///////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::onScreenRemoved(QScreen *screen) -{ - updateScreens(); -} - -///////////////////////////////////////////////////////////////////////////////////////// -void KonvergoWindow::updateCurrentScreen() -{ - QScreen* current = findCurrentScreen(); - QString currentName = current ? current->name() : ""; - if (currentName != m_currentScreenName) - updateScreens(); -} diff --git a/src/ui/KonvergoWindow.h b/src/ui/KonvergoWindow.h deleted file mode 100644 index 8066ed75e..000000000 --- a/src/ui/KonvergoWindow.h +++ /dev/null @@ -1,141 +0,0 @@ -#ifndef KONVERGOWINDOW_H -#define KONVERGOWINDOW_H - -#include -#include -#include - - -// This controls how big the web view will zoom using semantic zoom -// over a specific number of pixels and we run out of space for on screen -// tiles in chromium. This only happens on OSX since on other platforms -// we can use the GPU to transfer tiles directly but we set the limit on all platforms -// to keep it consistent. -// -// See more discussion in: https://github.com/plexinc/plex-media-player/issues/10 -// The number of pixels here are REAL pixels, the code in webview.qml will compensate -// for a higher DevicePixelRatio -// -#define WEBUI_MAX_HEIGHT 1440.0 -#define WEBUI_SIZE QSize(1280, 720) -#define WINDOWW_MIN_SIZE QSize(213, 120) - -/////////////////////////////////////////////////////////////////////////////////////////////////// -class KonvergoWindow : public QQuickWindow -{ - Q_OBJECT - - Q_PROPERTY(bool fullScreen READ isFullScreen WRITE setFullScreen NOTIFY fullScreenSwitched) - Q_PROPERTY(bool showDebugLayer MEMBER m_debugLayer NOTIFY debugLayerChanged) - Q_PROPERTY(QString debugInfo MEMBER m_debugInfo NOTIFY debugInfoChanged) - Q_PROPERTY(QString videoInfo MEMBER m_videoInfo NOTIFY debugInfoChanged) - Q_PROPERTY(QSize windowMinSize READ windowMinSize CONSTANT) - Q_PROPERTY(bool alwaysOnTop READ isAlwaysOnTop WRITE setAlwaysOnTop) - Q_PROPERTY(bool webDesktopMode MEMBER m_webDesktopMode NOTIFY webDesktopModeChanged) - Q_PROPERTY(QString webUrl READ webUrl NOTIFY webUrlChanged) - -public: - static void RegisterClass(); - - explicit KonvergoWindow(QWindow* parent = nullptr); - ~KonvergoWindow() override; - - bool isFullScreen() - { - return ((flags() & Qt::FramelessWindowHint) || (visibility() == QWindow::FullScreen)); - } - - Q_INVOKABLE void setFullScreen(bool enable); - - bool isAlwaysOnTop() - { - Qt::WindowFlags forceOnTopFlags = Qt::WindowStaysOnTopHint; -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - forceOnTopFlags = forceOnTopFlags | Qt::X11BypassWindowManagerHint; -#endif - return (flags() & forceOnTopFlags); - } - - void setAlwaysOnTop(bool enable); - - Q_SLOT void otherAppFocus() - { - setWindowState((Qt::WindowState)((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive)); - raise(); - } - - Q_SLOT void toggleDebug(); - - Q_SLOT void toggleFullscreen() { setFullScreen(!isFullScreen()); } - - Q_SLOT void toggleWebMode(); - - Q_SLOT void toggleAlwaysOnTop() - { - setAlwaysOnTop(!isAlwaysOnTop()); - } - - Q_SLOT void reloadWeb() - { - emit reloadWebClient(); - } - - Q_INVOKABLE Q_SLOT void minimizeWindow() - { - if (!isFullScreen()) - setVisibility(QWindow::Minimized); - } - - QSize windowMinSize() { return WINDOWW_MIN_SIZE; } - QString webUrl(); - -Q_SIGNALS: - void fullScreenSwitched(); - void enableVideoWindowSignal(); - void debugLayerChanged(); - void debugInfoChanged(); - void reloadWebClient(); - void webDesktopModeChanged(); - void webUrlChanged(); - -protected: - void focusOutEvent(QFocusEvent* ev) override; - void resizeEvent(QResizeEvent* event) override; - void keyPressEvent(QKeyEvent *ev) override; - -private slots: - void closingWindow(); - void enableVideoWindow(); - void onVisibilityChanged(QWindow::Visibility visibility); - void updateMainSectionSettings(const QVariantMap& values); - void updateWindowState(bool saveGeo = true); - void updateDebugInfo(); - void playerWindowVisible(bool visible); - void onScreenAdded(QScreen *screen); - void onScreenRemoved(QScreen *screen); - void updateCurrentScreen(); - -private: - void saveGeometry(); - QRect loadGeometry(); - QRect loadGeometryRect(); - bool fitsInScreens(const QRect& rc); - QScreen* loadLastScreen(); - void updateScreens(); - void updateForcedScreen(); - QScreen* findCurrentScreen(); - - bool m_debugLayer; - QTimer* m_infoTimer; - QString m_debugInfo, m_systemDebugInfo, m_videoInfo; - int m_ignoreFullscreenSettingsChange; - bool m_webDesktopMode; - bool m_showedUpdateDialog; - - unsigned long m_osxPresentationOptions; - QString m_currentScreenName; - - void setWebMode(bool newDesktopMode, bool fullscreen); -}; - -#endif // KONVERGOWINDOW_H diff --git a/src/ui/WindowManager.cpp b/src/ui/WindowManager.cpp new file mode 100644 index 000000000..9331bb73d --- /dev/null +++ b/src/ui/WindowManager.cpp @@ -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 +#include +#include + +/////////////////////////////////////////////////////////////////////////////////////////////////// +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); +} diff --git a/src/ui/WindowManager.h b/src/ui/WindowManager.h new file mode 100644 index 000000000..e547457e2 --- /dev/null +++ b/src/ui/WindowManager.h @@ -0,0 +1,90 @@ +#ifndef WINDOWMANAGER_H +#define WINDOWMANAGER_H + +#include +#include +#include +#include +#include +#include +#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 diff --git a/src/ui/webview.qml b/src/ui/webview.qml index cee6855b8..de8865242 100644 --- a/src/ui/webview.qml +++ b/src/ui/webview.qml @@ -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 + WebChannel { - id: video - objectName: "video" - // It's not a real item. Its renderer draws onto the view's background. - width: 0 - height: 0 - visible: false + id: webChannelObject } - WebChannel + MpvVideoItem { - id: webChannelObject + 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) } } } diff --git a/src/utils/CachedRegexMatcher.cpp b/src/utils/CachedRegexMatcher.cpp index c5f123b53..4c9a63ad0 100644 --- a/src/utils/CachedRegexMatcher.cpp +++ b/src/utils/CachedRegexMatcher.cpp @@ -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); } diff --git a/src/utils/CachedRegexMatcher.h b/src/utils/CachedRegexMatcher.h index ef4619a79..969fd5a16 100644 --- a/src/utils/CachedRegexMatcher.h +++ b/src/utils/CachedRegexMatcher.h @@ -5,12 +5,12 @@ #ifndef KONVERGO_CACHEDREGEXMATCHER_H #define KONVERGO_CACHEDREGEXMATCHER_H -#include +#include #include #include #include -typedef QPair MatcherValuePair; +typedef QPair MatcherValuePair; typedef QList MatcherValueList; class CachedRegexMatcher : public QObject diff --git a/src/utils/Log.cpp b/src/utils/Log.cpp index 06c6e3c40..206153f3a 100644 --- a/src/utils/Log.cpp +++ b/src/utils/Log.cpp @@ -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; } diff --git a/src/utils/PlatformUtils.cpp b/src/utils/PlatformUtils.cpp new file mode 100644 index 000000000..cd466f720 --- /dev/null +++ b/src/utils/PlatformUtils.cpp @@ -0,0 +1,19 @@ +// +// Created by Tobias Hieta on 15/05/15. +// + +#include "PlatformUtils.h" + +#ifdef Q_OS_UNIX +#include +#endif + +bool PlatformUtils::isProcessAlive(qint64 pid) +{ +#ifdef Q_OS_UNIX + int ret = kill((pid_t)pid, 0); + return ret == 0; +#endif + + return false; +} diff --git a/src/utils/PlatformUtils.h b/src/utils/PlatformUtils.h new file mode 100644 index 000000000..ca23fea35 --- /dev/null +++ b/src/utils/PlatformUtils.h @@ -0,0 +1,18 @@ +// +// Created by Tobias Hieta on 15/05/15. +// + +#ifndef KONVERGO_PLATFORMUTILS_H +#define KONVERGO_PLATFORMUTILS_H + +#include + +class PlatformUtils +{ +public: + PlatformUtils() { } + static bool isProcessAlive(qint64 pid); +}; + + +#endif //KONVERGO_PLATFORMUTILS_H diff --git a/src/utils/Utils.cpp b/src/utils/Utils.cpp index a2c88799f..ca47c4589 100644 --- a/src/utils/Utils.cpp +++ b/src/utils/Utils.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include @@ -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); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4b9524eaf..00521b041 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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}