Skip to content
This repository was archived by the owner on Mar 28, 2026. It is now read-only.

Commit 892d17d

Browse files
committed
Add HDR playback on Wayland
1 parent 11cbc15 commit 892d17d

22 files changed

+4997
-53
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "external/mpvqt"]
22
path = external/mpvqt
33
url = https://invent.kde.org/libraries/mpvqt.git
4+
[submodule "external/mpv"]
5+
path = external/mpv
6+
url = https://github.com/andrewrabert/mpv

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ option(CHECK_FOR_UPDATES "Enable checking for new versions of Jellyfin" OFF)
1414
option(OPENELEC "Make an OpenELEC build" OFF)
1515
option(LINUX_X11POWER "Enable non D-Bus screensaver management" OFF)
1616
option(USE_STATIC_MPVQT "Build MpvQt from bundled submodule instead of using system library" OFF)
17+
option(USE_WAYLAND_SUBSURFACE "Use Wayland subsurface for mpv rendering (Wayland-only, enables HDR)" OFF)
1718

1819
if (NOT CHECK_FOR_UPDATES)
1920
add_definitions(-DDISABLE_UPDATE_CHECK)
Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,44 @@
1-
# We want OpenGL or OpenGLES2
2-
find_package(OpenGL)
3-
if(NOT OPENGL_FOUND)
4-
find_package(GLES2)
5-
if(NOT GLES2_FOUND)
6-
message(FATAL_ERROR "OpenGL or GLES2 is required")
7-
else(NOT GLES2_FOUND)
8-
set(OPENGL_LIBS ${GLES2_LIBRARY})
9-
endif(NOT GLES2_FOUND)
10-
else(NOT OPENGL_FOUND)
11-
set(OPENGL_LIBS ${OPENGL_gl_LIBRARY})
12-
endif(NOT OPENGL_FOUND)
13-
14-
find_package(MPV REQUIRED)
15-
include_directories(${MPV_INCLUDE_DIR})
1+
if(USE_WAYLAND_SUBSURFACE)
2+
message(STATUS "Using Wayland subsurface for mpv rendering (HDR enabled)")
3+
find_package(Vulkan REQUIRED)
4+
find_package(PkgConfig REQUIRED)
5+
pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client)
6+
find_package(Qt6 REQUIRED COMPONENTS GuiPrivate)
7+
8+
include(WaylandMpvConfiguration)
9+
include_directories(${MPV_INCLUDE_DIR})
10+
include_directories(${WAYLAND_CLIENT_INCLUDE_DIRS})
11+
12+
add_compile_definitions(USE_WAYLAND_SUBSURFACE)
13+
14+
set(WAYLAND_MPV_SOURCES
15+
${CMAKE_SOURCE_DIR}/src/player/WaylandVulkanContext.cpp
16+
${CMAKE_SOURCE_DIR}/src/player/WaylandMpvPlayer.cpp
17+
${CMAKE_SOURCE_DIR}/src/player/wayland-protocols/color-management-v1.c
18+
)
19+
set(WAYLAND_MPV_LIBS
20+
${MPV_LIBRARY}
21+
Vulkan::Vulkan
22+
${WAYLAND_CLIENT_LIBRARIES}
23+
Qt6::GuiPrivate
24+
)
25+
else()
26+
# We want OpenGL or OpenGLES2
27+
find_package(OpenGL)
28+
if(NOT OPENGL_FOUND)
29+
find_package(GLES2)
30+
if(NOT GLES2_FOUND)
31+
message(FATAL_ERROR "OpenGL or GLES2 is required")
32+
else(NOT GLES2_FOUND)
33+
set(OPENGL_LIBS ${GLES2_LIBRARY})
34+
endif(NOT GLES2_FOUND)
35+
else(NOT OPENGL_FOUND)
36+
set(OPENGL_LIBS ${OPENGL_gl_LIBRARY})
37+
endif(NOT OPENGL_FOUND)
38+
39+
find_package(MPV REQUIRED)
40+
include_directories(${MPV_INCLUDE_DIR})
41+
42+
set(WAYLAND_MPV_SOURCES "")
43+
set(WAYLAND_MPV_LIBS "")
44+
endif()

CMakeModules/QtConfiguration.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ if(LINUX_DBUS)
3434
set(components ${components} DBus)
3535
endif(LINUX_DBUS)
3636

37+
if(USE_WAYLAND_SUBSURFACE)
38+
# We use raw Wayland protocol for color management, not Qt's private classes
39+
endif()
40+
3741
foreach(COMP ${components})
3842
set(mod Qt6${COMP})
3943

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Build mpv from submodule for Wayland subsurface rendering
2+
3+
set(MPV_SOURCE_DIR "${CMAKE_SOURCE_DIR}/external/mpv")
4+
set(MPV_BUILD_DIR "${MPV_SOURCE_DIR}/build")
5+
6+
if(NOT EXISTS "${MPV_BUILD_DIR}/libmpv.so")
7+
message(STATUS "Building mpv from submodule...")
8+
execute_process(
9+
COMMAND meson setup build --default-library=shared -Dlibmpv=true
10+
WORKING_DIRECTORY ${MPV_SOURCE_DIR}
11+
RESULT_VARIABLE MPV_SETUP_RESULT
12+
)
13+
if(NOT MPV_SETUP_RESULT EQUAL 0)
14+
message(FATAL_ERROR "Failed to configure mpv with meson")
15+
endif()
16+
execute_process(
17+
COMMAND meson compile -C build
18+
WORKING_DIRECTORY ${MPV_SOURCE_DIR}
19+
RESULT_VARIABLE MPV_BUILD_RESULT
20+
)
21+
if(NOT MPV_BUILD_RESULT EQUAL 0)
22+
message(FATAL_ERROR "Failed to build mpv")
23+
endif()
24+
endif()
25+
26+
set(MPV_INCLUDE_DIR "${MPV_SOURCE_DIR}/include")
27+
set(MPV_LIBRARY "${MPV_BUILD_DIR}/libmpv.so")
28+
29+
# Set RPATH for finding bundled libmpv
30+
set(CMAKE_BUILD_RPATH "${MPV_BUILD_DIR}")
31+
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")

external/CMakeLists.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,28 @@ if(USE_STATIC_MPVQT)
3131
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/MpvQt/MpvAbstractItem "#include \"mpvabstractitem.h\"\n")
3232
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/MpvQt/MpvController "#include \"mpvcontroller.h\"\n")
3333
endif()
34+
35+
# Build mpv from submodule using meson
36+
set(MPV_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/mpv")
37+
set(MPV_BUILD_DIR "${MPV_SOURCE_DIR}/build")
38+
39+
if(NOT EXISTS "${MPV_BUILD_DIR}/libmpv.so")
40+
message(STATUS "Building mpv from submodule...")
41+
execute_process(
42+
COMMAND meson setup build --default-library=shared -Dlibmpv=true
43+
WORKING_DIRECTORY ${MPV_SOURCE_DIR}
44+
RESULT_VARIABLE MPV_SETUP_RESULT
45+
)
46+
if(NOT MPV_SETUP_RESULT EQUAL 0)
47+
message(FATAL_ERROR "Failed to configure mpv with meson")
48+
endif()
49+
execute_process(
50+
COMMAND meson compile -C build
51+
WORKING_DIRECTORY ${MPV_SOURCE_DIR}
52+
RESULT_VARIABLE MPV_BUILD_RESULT
53+
)
54+
if(NOT MPV_BUILD_RESULT EQUAL 0)
55+
message(FATAL_ERROR "Failed to build mpv")
56+
endif()
57+
endif()
58+

external/mpv

Submodule mpv added at c9fd9b8

src/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ foreach(sfile in ${ALL_SRCS})
5454
endforeach(sfile in ${ALL_SRCS})
5555

5656
# Build core sources as static library for reuse by tests
57-
add_library(jmp_core STATIC ${ALL_SRCS})
57+
add_library(jmp_core STATIC ${ALL_SRCS} ${WAYLAND_MPV_SOURCES})
5858

5959
file(GLOB_RECURSE RESOURCE_FILES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/resources/*)
6060
file(GLOB_RECURSE NATIVE_FILES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/native/*)
@@ -189,6 +189,7 @@ target_link_libraries(jmp_core
189189
shared
190190
${MPV_LIBRARY}
191191
${OPENGL_LIBS}
192+
${WAYLAND_MPV_LIBS}
192193
${QT6_LIBRARIES}
193194
${OS_LIBS}
194195
${EXTRA_LIBS}

src/main.cpp

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
#include <QGuiApplication>
22
#include <QApplication>
3+
#include <memory>
34
#include <QDateTime>
45
#include <QFileInfo>
56
#include <QIcon>
67
#include <QtQml>
78
#include <optional>
89
#include <Qt>
910
#include <QtWebEngineQuick>
11+
#ifdef USE_WAYLAND_SUBSURFACE
12+
#include <QVulkanInstance>
13+
#endif
1014
#include <qtwebenginecoreglobal.h>
1115
#include <QtWebEngineCore/QWebEngineProfile>
1216
#include <QErrorMessage>
@@ -127,12 +131,6 @@ void ShowLicenseInfo()
127131
printf("%.*s\n", static_cast<int>(contents.size()), contents.data());
128132
}
129133

130-
/////////////////////////////////////////////////////////////////////////////////////////
131-
QStringList g_qtFlags = {
132-
"--enable-gpu-rasterization",
133-
"--disable-features=MediaSessionService"
134-
};
135-
136134
/////////////////////////////////////////////////////////////////////////////////////////
137135
int main(int argc, char *argv[])
138136
{
@@ -198,11 +196,39 @@ int main(int argc, char *argv[])
198196
parser.addOption(deleteProfileOption);
199197
parser.addOption(createProfileOption);
200198

199+
#ifdef USE_WAYLAND_SUBSURFACE
200+
// Check if we'll use Wayland Vulkan mode - needs to be early for Qt flags
201+
static bool isWayland = qEnvironmentVariable("XDG_SESSION_TYPE") == "wayland" ||
202+
qEnvironmentVariable("QT_QPA_PLATFORM").contains("wayland");
203+
#endif
204+
205+
// Qt flags for WebEngine - skip on Wayland Vulkan to avoid conflicts
206+
QStringList g_qtFlags;
207+
#ifdef USE_WAYLAND_SUBSURFACE
208+
if (!isWayland) {
209+
g_qtFlags << "--enable-gpu-rasterization" << "--disable-features=MediaSessionService";
210+
}
211+
#else
212+
g_qtFlags << "--enable-gpu-rasterization" << "--disable-features=MediaSessionService";
213+
#endif
214+
201215
char **newArgv = appendCommandLineArguments(argc, argv, g_qtFlags);
202216
int newArgc = argc + g_qtFlags.size();
203217

204218
preinitQt();
205219

220+
#ifdef USE_WAYLAND_SUBSURFACE
221+
if (!isWayland) {
222+
detectOpenGLEarly();
223+
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
224+
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
225+
}
226+
#else
227+
detectOpenGLEarly();
228+
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
229+
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
230+
#endif
231+
206232
QStringList arguments;
207233
for (int i = 0; i < argc; i++)
208234
arguments << QString::fromLatin1(argv[i]);
@@ -230,10 +256,6 @@ int main(int argc, char *argv[])
230256
#endif
231257
}
232258

233-
detectOpenGLEarly();
234-
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
235-
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
236-
237259
if (parser.isSet("help"))
238260
{
239261
// Get Qt's generated help, insert section header before profile options
@@ -423,9 +445,17 @@ int main(int argc, char *argv[])
423445
#endif
424446

425447
QStringList chromiumFlags;
448+
#ifdef USE_WAYLAND_SUBSURFACE
449+
if (isWayland) {
450+
// Disable GPU compositing in Chromium - jellyfin-web triggers Vulkan crash with radeon
451+
chromiumFlags << "--disable-gpu-compositing";
452+
} else {
453+
#endif
426454
#ifdef Q_OS_LINUX
427-
// Disable QtWebEngine's automatic MPRIS registration - we handle it ourselves
428-
chromiumFlags << "--disable-features=MediaSessionService,HardwareMediaKeyHandling";
455+
chromiumFlags << "--disable-features=MediaSessionService,HardwareMediaKeyHandling";
456+
#endif
457+
#ifdef USE_WAYLAND_SUBSURFACE
458+
}
429459
#endif
430460
// Disable pinch-to-zoom if browser zoom is not allowed
431461
QVariant allowZoom = SettingsComponent::readPreinitValue(SETTINGS_SECTION_MAIN, "allowBrowserZoom");
@@ -441,27 +471,65 @@ int main(int argc, char *argv[])
441471
if (parser.isSet("remote-debugging-port"))
442472
qputenv("QTWEBENGINE_REMOTE_DEBUGGING", parser.value("remote-debugging-port").toUtf8());
443473

474+
// Must initialize QtWebEngine before QGuiApplication (per Qt docs)
444475
QtWebEngineQuick::initialize();
476+
477+
#ifdef USE_WAYLAND_SUBSURFACE
478+
// Set Vulkan graphics API for Wayland (must be after QtWebEngineQuick::initialize, before QApplication)
479+
if (isWayland) {
480+
QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan);
481+
}
482+
#endif
483+
484+
#ifdef USE_WAYLAND_SUBSURFACE
485+
// Use QGuiApplication for Vulkan (QApplication's widget stuff conflicts with Vulkan)
486+
std::unique_ptr<QCoreApplication> appPtr;
487+
if (isWayland) {
488+
appPtr.reset(new QGuiApplication(newArgc, newArgv));
489+
} else {
490+
appPtr.reset(new QApplication(newArgc, newArgv));
491+
}
492+
auto& app = *appPtr;
493+
#else
445494
QApplication app(newArgc, newArgv);
495+
#endif
496+
497+
#ifdef USE_WAYLAND_SUBSURFACE
498+
// Create Vulkan instance for Qt (must be after QApplication, before QML engine)
499+
static QVulkanInstance vulkanInstance;
500+
if (isWayland) {
501+
vulkanInstance.setApiVersion(QVersionNumber(1, 2));
502+
if (!vulkanInstance.create()) {
503+
qCritical() << "Failed to create Qt Vulkan instance";
504+
return EXIT_FAILURE;
505+
}
506+
}
507+
#endif
446508

447-
#if defined(Q_OS_WIN)
509+
#if defined(Q_OS_WIN)
448510
// Setting window icon on OSX will break user ability to change it
449-
app.setWindowIcon(QIcon(":/images/icon.png"));
511+
qobject_cast<QGuiApplication*>(&app)->setWindowIcon(QIcon(":/images/icon.png"));
450512
#endif
451513

452514
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
453515
// Set window icon on Linux using system icon theme
454-
app.setWindowIcon(QIcon::fromTheme("org.jellyfin.JellyfinDesktop", QIcon(":/images/icon.png")));
516+
qobject_cast<QGuiApplication*>(&app)->setWindowIcon(QIcon::fromTheme("org.jellyfin.JellyfinDesktop", QIcon(":/images/icon.png")));
455517
// Set app id for Wayland compositor window icon
456-
app.setDesktopFileName("org.jellyfin.JellyfinDesktop");
518+
qobject_cast<QGuiApplication*>(&app)->setDesktopFileName("org.jellyfin.JellyfinDesktop");
457519
#endif
458520

459521
// Configure default WebEngineProfile paths early (profile is already set)
460-
{
522+
// Skip early access on Wayland Vulkan - accessing defaultProfile() here triggers
523+
// Chromium GPU init which crashes. Will be set later after QML engine loads.
524+
#ifdef USE_WAYLAND_SUBSURFACE
525+
if (!isWayland) {
526+
#endif
461527
QWebEngineProfile* defaultProfile = QWebEngineProfile::defaultProfile();
462528
defaultProfile->setCachePath(ProfileManager::activeProfile().cacheDir("QtWebEngine"));
463529
defaultProfile->setPersistentStoragePath(ProfileManager::activeProfile().dataDir("QtWebEngine"));
530+
#ifdef USE_WAYLAND_SUBSURFACE
464531
}
532+
#endif
465533

466534
#if defined(Q_OS_MAC) && defined(NDEBUG)
467535
PFMoveToApplicationsFolderIfNecessary();
@@ -476,7 +544,7 @@ int main(int argc, char *argv[])
476544

477545
#ifdef Q_OS_UNIX
478546
// install signals handlers for proper app closing.
479-
SignalManager signalManager(&app);
547+
SignalManager signalManager(qobject_cast<QGuiApplication*>(&app));
480548
Q_UNUSED(signalManager);
481549
#endif
482550

@@ -497,7 +565,14 @@ int main(int argc, char *argv[])
497565
SettingsComponent::Get().setCommandLineValues(parser.optionNames());
498566

499567
// Set user agent now that SystemComponent is available
500-
QWebEngineProfile::defaultProfile()->setHttpUserAgent(SystemComponent::Get().getUserAgent());
568+
// Skip early access on Wayland Vulkan - will be set in objectCreated callback
569+
#ifdef USE_WAYLAND_SUBSURFACE
570+
if (!isWayland) {
571+
#endif
572+
QWebEngineProfile::defaultProfile()->setHttpUserAgent(SystemComponent::Get().getUserAgent());
573+
#ifdef USE_WAYLAND_SUBSURFACE
574+
}
575+
#endif
501576

502577
// load QtWebChannel so that we can register our components with it.
503578
QQmlApplicationEngine *engine = Globals::Engine();
@@ -517,6 +592,20 @@ int main(int argc, char *argv[])
517592

518593
QQuickWindow* window = Globals::MainWindow();
519594

595+
#ifdef USE_WAYLAND_SUBSURFACE
596+
// Set Vulkan instance on window when using Vulkan
597+
if (isWayland && vulkanInstance.isValid()) {
598+
window->setVulkanInstance(&vulkanInstance);
599+
}
600+
// Deferred WebEngineProfile setup for Wayland Vulkan
601+
if (isWayland) {
602+
QWebEngineProfile* defaultProfile = QWebEngineProfile::defaultProfile();
603+
defaultProfile->setCachePath(ProfileManager::activeProfile().cacheDir("QtWebEngine"));
604+
defaultProfile->setPersistentStoragePath(ProfileManager::activeProfile().dataDir("QtWebEngine"));
605+
defaultProfile->setHttpUserAgent(SystemComponent::Get().getUserAgent());
606+
}
607+
#endif
608+
520609
// Set window flags for proper popup handling (e.g., WebEngineView dropdowns)
521610
window->setFlags(window->flags() | Qt::WindowFullscreenButtonHint);
522611

0 commit comments

Comments
 (0)