Linux: use GTK sink for Wayland Bambu liveview (#13432)

* Use GTK sink for Wayland liveview

Keep native Wayland sessions on the GTK backend and use a GTK widget based GStreamer sink for Bambu liveview instead of the Wayland video overlay path, which can render black on NVIDIA/Hyprland.

Keep the existing wxMediaCtrl path for X11 and continue preferring software H.264 decoding while demoting GL and hardware decoder paths that caused liveview crashes.

* Narrow Linux liveview fix to native Wayland

* Tighten native Wayland liveview setup

* Tighten Wayland liveview teardown and rank setup

* Package GStreamer gtksink for Wayland liveview

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
This commit is contained in:
Thomas Henauer
2026-05-12 18:01:10 +02:00
committed by GitHub
parent e90e22ae82
commit a167702038
10 changed files with 397 additions and 15 deletions

View File

@@ -35,12 +35,13 @@ finish-args:
modules:
# JPEG codec for the liveview
# JPEG codec and GTK video sink (Wayland liveview) for the liveview
- name: gst-plugins-good
buildsystem: meson
config-opts:
- -Dauto_features=disabled
- -Djpeg=enabled
- -Dgtk3=enabled
- -Ddoc=disabled
- -Dexamples=disabled
- -Dtests=disabled

View File

@@ -12,6 +12,7 @@ export REQUIRED_DEV_PACKAGES=(
gettext
git
glew
gst-plugins-good
gstreamer
gstreamermm
gtk3

View File

@@ -12,6 +12,7 @@ export REQUIRED_DEV_PACKAGES=(
gettext
git
glew
gst-plugins-good
gstreamer
gtk3
libmspack

View File

@@ -10,6 +10,7 @@ REQUIRED_DEV_PACKAGES=(
g++
gettext
git
gstreamer1.0-gtk3
libcurl4-openssl-dev
libdbus-1-dev
libglew-dev

View File

@@ -13,6 +13,7 @@ REQUIRED_DEV_PACKAGES=(
gettext
git
gstreamer1-devel
gstreamer1-plugins-good-gtk
gstreamermm-devel
gtk3-devel
libmspack-devel

View File

@@ -21,6 +21,7 @@ REQUIRED_DEV_PACKAGES=(
media-libs/glew
media-libs/gst-plugins-base:1.0
media-libs/gstreamer:1.0
media-plugins/gst-plugins-gtk:1.0
net-misc/curl
net-misc/wget
sys-apps/dbus

View File

@@ -13,6 +13,7 @@ REQUIRED_DEV_PACKAGES=(
gettext
git
gstreamer-devel
gstreamer-plugins-good-gtk
gtk3-devel
libmspack-devel
libquadmath-devel

View File

@@ -32,7 +32,8 @@ static std::map<int, std::string> error_messages = {
{100, L("The player is not loaded, please click \"play\" button to retry.")},
{101, L("The player is not loaded, please click \"play\" button to retry.")},
{102, L("The player is not loaded, please click \"play\" button to retry.")},
{103, L("The player is not loaded, please click \"play\" button to retry.")}
{103, L("The player is not loaded, please click \"play\" button to retry.")},
{104, L("The player is not loaded because the GStreamer GTK video sink is missing or failed to initialize.")}
};
namespace Slic3r {
@@ -838,6 +839,12 @@ void wxMediaCtrl2::DoSetSize(int x, int y, int width, int height, int sizeFlags)
wxWindow::DoSetSize(x, y, width, height, sizeFlags);
#else
wxMediaCtrl::DoSetSize(x, y, width, height, sizeFlags);
#endif
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_gtk_video_window) {
const wxSize client_size = GetClientSize();
m_gtk_video_window->SetSize(0, 0, client_size.GetWidth(), client_size.GetHeight());
}
#endif
if (sizeFlags & wxSIZE_USE_EXISTING) return;
wxSize size = m_video_size;
@@ -853,4 +860,3 @@ void wxMediaCtrl2::DoSetSize(int x, int y, int width, int height, int sizeFlags)
});
}
}

View File

@@ -2,7 +2,9 @@
#include "libslic3r/Time.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
#include "LinuxDisplayBackend.hpp"
#include <boost/filesystem/operations.hpp>
#include <string>
#ifdef __WIN32__
#include <winuser.h>
#include <versionhelpers.h>
@@ -13,6 +15,78 @@
#ifdef __LINUX__
#include "Printer/gstbambusrc.h"
#include <gst/gst.h> // main gstreamer header
#endif
#if defined(__LINUX__) && defined(__WXGTK__)
#include <gtk/gtk.h>
#include <wx/nativewin.h>
namespace {
bool ensure_gstreamer_initialized_for_liveview()
{
GError* error = nullptr;
if (!gst_init_check(nullptr, nullptr, &error)) {
BOOST_LOG_TRIVIAL(error) << "wxMediaCtrl2: gst_init_check failed before native Wayland liveview setup"
<< (error ? std::string(": ") + error->message : std::string());
if (error)
g_error_free(error);
return false;
}
return true;
}
bool is_gstreamer_feature_available(const char* feature)
{
if (!ensure_gstreamer_initialized_for_liveview())
return false;
GstElementFactory* factory = gst_element_factory_find(feature);
if (!factory)
return false;
gst_object_unref(factory);
return true;
}
void set_gstreamer_feature_rank(const char* feature, guint rank)
{
GstElementFactory* factory = gst_element_factory_find(feature);
if (!factory)
return;
gst_plugin_feature_set_rank(GST_PLUGIN_FEATURE(factory), rank);
gst_object_unref(factory);
}
void configure_wayland_gstreamer_liveview_path()
{
static bool configured = false;
if (configured)
return;
configured = true;
if (!ensure_gstreamer_initialized_for_liveview())
return;
// Prefer software decode for Bambu liveview on Wayland/NVIDIA, where
// zero-copy GL/DMABUF display paths can be fragile. Keep hardware
// decoders available as lower-ranked fallbacks for VAAPI/NVDEC/V4L2-only
// installations instead of passing preflight and then blocking autoplug.
set_gstreamer_feature_rank("avdec_h264", GST_RANK_PRIMARY + 300);
set_gstreamer_feature_rank("openh264dec", GST_RANK_PRIMARY + 100);
set_gstreamer_feature_rank("nvh264dec", GST_RANK_MARGINAL);
set_gstreamer_feature_rank("vaapih264dec", GST_RANK_MARGINAL);
set_gstreamer_feature_rank("vah264dec", GST_RANK_MARGINAL);
set_gstreamer_feature_rank("v4l2h264dec", GST_RANK_MARGINAL);
}
}
#endif // defined(__LINUX__) && defined(__WXGTK__)
#ifdef __LINUX__
extern "C" int gst_bambu_last_error;
class WXDLLIMPEXP_MEDIA
wxGStreamerMediaBackend : public wxMediaBackendCommonBase
{
@@ -25,6 +99,15 @@ wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
wxMediaCtrl2::wxMediaCtrl2(wxWindow *parent)
{
#if defined(__LINUX__) && defined(__WXGTK__)
m_native_wayland = Slic3r::GUI::is_running_on_wayland();
if (m_native_wayland && is_gstreamer_feature_available("gtksink"))
configure_wayland_gstreamer_liveview_path();
else if (m_native_wayland) {
m_gtk_sink_error = _L("Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer.");
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: native Wayland liveview disabled because GStreamer gtksink is unavailable";
}
#endif
#ifdef __WIN32__
auto hModExe = GetModuleHandle(NULL);
// BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: GetModuleHandle " << hModExe;
@@ -38,21 +121,203 @@ wxMediaCtrl2::wxMediaCtrl2(wxWindow *parent)
// BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: AmdPowerXpressRequestHighPerformance " << *AmdPowerXpressRequestHighPerformance;
*AmdPowerXpressRequestHighPerformance = 0;
}
#endif
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_native_wayland)
wxControl::Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize);
else
#endif
wxMediaCtrl::Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxMEDIACTRLPLAYERCONTROLS_NONE);
#ifdef __LINUX__
/* Register only after we have created the wxMediaCtrl, since only then are we guaranteed to have fired up Gstreamer's plugin registry. */
auto playbin = reinterpret_cast<wxGStreamerMediaBackend *>(m_imp)->m_playbin;
g_object_set (G_OBJECT (playbin),
"audio-sink", NULL,
NULL);
gstbambusrc_register();
#ifdef __WXGTK__
if (m_native_wayland && m_gtk_sink_error.empty())
m_use_gtk_sink = CreateGtkSinkPlayer();
if (m_native_wayland && !m_use_gtk_sink && m_gtk_sink_error.empty())
m_gtk_sink_error = _L("Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation.");
#endif
if (!m_use_gtk_sink && m_imp) {
auto playbin = reinterpret_cast<wxGStreamerMediaBackend *>(m_imp)->m_playbin;
g_object_set(G_OBJECT(playbin),
"audio-sink", nullptr,
nullptr);
} else if (!m_use_gtk_sink) {
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: wxMediaCtrl backend is unavailable";
}
Bind(wxEVT_MEDIA_LOADED, [this](auto & e) {
m_loaded = true;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(0);
event.SetEventObject(this);
wxPostEvent(this, event);
});
#endif
}
wxMediaCtrl2::~wxMediaCtrl2()
{
#if defined(__LINUX__) && defined(__WXGTK__)
DestroyGtkSinkPlayer();
#endif
}
#if defined(__LINUX__) && defined(__WXGTK__)
bool wxMediaCtrl2::CreateGtkSinkPlayer()
{
GstElement *playbin = gst_element_factory_make("playbin", "orca-wayland-gtk-playbin");
if (!playbin)
return false;
GError *error = nullptr;
GstElement *video_sink = gst_parse_bin_from_description(
"videoconvert ! videoscale ! video/x-raw,format=BGRx ! gtksink name=orca_wayland_gtksink sync=false",
TRUE,
&error);
if (!video_sink) {
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: failed to create gtksink video bin"
<< (error ? std::string(": ") + error->message : std::string());
if (error)
g_error_free(error);
gst_object_unref(playbin);
return false;
}
GstElement *gtk_sink = gst_bin_get_by_name(GST_BIN(video_sink), "orca_wayland_gtksink");
if (!gtk_sink) {
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: failed to find gtksink in video bin";
gst_object_unref(video_sink);
gst_object_unref(playbin);
return false;
}
GtkWidget *gtk_widget = nullptr;
g_object_get(G_OBJECT(gtk_sink), "widget", &gtk_widget, nullptr);
if (!gtk_widget) {
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: gtksink did not expose a GtkWidget";
gst_object_unref(gtk_sink);
gst_object_unref(video_sink);
gst_object_unref(playbin);
return false;
}
gtk_widget_show(gtk_widget);
m_gtk_video_window = new wxNativeWindow(this, wxID_ANY, gtk_widget);
m_gtk_video_window->Show();
g_object_unref(gtk_widget);
g_object_set(G_OBJECT(playbin),
"video-sink", video_sink,
"audio-sink", nullptr,
nullptr);
gst_object_unref(video_sink);
m_gtk_playbin = playbin;
m_gtk_sink = gtk_sink;
GstBus *bus = gst_element_get_bus(playbin);
m_gtk_bus_watch_id = gst_bus_add_watch(bus, [](GstBus *, GstMessage *message, gpointer data) -> gboolean {
auto *self = static_cast<wxMediaCtrl2 *>(data);
if (!self || !self->m_gtk_playbin)
return G_SOURCE_REMOVE;
switch (GST_MESSAGE_TYPE(message)) {
case GST_MESSAGE_ERROR:
{
GError *error = nullptr;
gchar *debug = nullptr;
gst_message_parse_error(message, &error, &debug);
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: gtksink pipeline error"
<< (error ? std::string(": ") + error->message : std::string())
<< (debug ? std::string(" debug: ") + debug : std::string());
if (error)
g_error_free(error);
if (debug)
g_free(debug);
self->m_error = gst_bambu_last_error ? gst_bambu_last_error : 2;
self->m_loaded = false;
self->m_gtk_state = wxMEDIASTATE_STOPPED;
self->PostGtkSinkStateEvent(self->GetId());
break;
}
case GST_MESSAGE_EOS:
self->m_loaded = false;
self->m_gtk_state = wxMEDIASTATE_STOPPED;
self->PostGtkSinkStateEvent(self->GetId());
break;
case GST_MESSAGE_STATE_CHANGED:
if (GST_MESSAGE_SRC(message) == GST_OBJECT(self->m_gtk_playbin)) {
GstState old_state;
GstState new_state;
GstState pending_state;
gst_message_parse_state_changed(message, &old_state, &new_state, &pending_state);
if (new_state == GST_STATE_PLAYING) {
self->m_loaded = true;
self->m_gtk_state = wxMEDIASTATE_PLAYING;
self->PostGtkSinkStateEvent();
} else if (new_state == GST_STATE_PAUSED && old_state < GST_STATE_PAUSED) {
// Treat only upward READY/NULL -> PAUSED as load completion.
// PLAYING -> PAUSED is a normal teardown step before NULL.
self->m_loaded = true;
self->m_gtk_state = wxMEDIASTATE_PAUSED;
self->PostGtkSinkStateEvent();
} else if (new_state <= GST_STATE_READY && old_state >= GST_STATE_PAUSED) {
self->m_loaded = false;
self->m_gtk_state = wxMEDIASTATE_STOPPED;
self->PostGtkSinkStateEvent();
}
}
break;
default:
break;
}
return G_SOURCE_CONTINUE;
}, this);
gst_object_unref(bus);
BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: using GTK native Wayland video sink";
return true;
}
void wxMediaCtrl2::DestroyGtkSinkPlayer()
{
if (m_gtk_bus_watch_id) {
g_source_remove(m_gtk_bus_watch_id);
m_gtk_bus_watch_id = 0;
}
if (m_gtk_playbin) {
gst_element_set_state(m_gtk_playbin, GST_STATE_NULL);
}
if (m_gtk_video_window) {
m_gtk_video_window->Destroy();
m_gtk_video_window = nullptr;
}
if (m_gtk_playbin) {
gst_object_unref(m_gtk_playbin);
m_gtk_playbin = nullptr;
}
if (m_gtk_sink) {
gst_object_unref(m_gtk_sink);
m_gtk_sink = nullptr;
}
}
void wxMediaCtrl2::PostGtkSinkStateEvent(int id)
{
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(id);
event.SetEventObject(this);
wxPostEvent(this, event);
}
#endif // defined(__LINUX__) && defined(__WXGTK__)
#define CLSID_BAMBU_SOURCE L"{233E64FB-2041-4A6C-AFAB-FF9BCF83E7AA}"
void wxMediaCtrl2::Load(wxURI url)
@@ -178,6 +443,15 @@ void wxMediaCtrl2::Load(wxURI url)
if (!factory) {
factory = gst_element_factory_find("vaapih264dec");
}
if (!factory) {
factory = gst_element_factory_find("vah264dec");
}
if (!factory) {
factory = gst_element_factory_find("nvh264dec");
}
if (!factory) {
factory = gst_element_factory_find("v4l2h264dec");
}
if (!factory) {
hasplugins = 0;
} else {
@@ -199,30 +473,103 @@ void wxMediaCtrl2::Load(wxURI url)
#endif
m_error = 0;
m_loaded = false;
wxMediaCtrl::Load(url);
#ifdef __WXGTK3__
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_use_gtk_sink && m_gtk_playbin) {
const std::string uri = std::string(url.BuildURI().ToUTF8().data());
gst_element_set_state(m_gtk_playbin, GST_STATE_NULL);
g_object_set(G_OBJECT(m_gtk_playbin), "uri", uri.c_str(), nullptr);
m_gtk_state = wxMEDIASTATE_STOPPED;
GstStateChangeReturn state = gst_element_set_state(m_gtk_playbin, GST_STATE_PAUSED);
if (state == GST_STATE_CHANGE_FAILURE) {
m_error = gst_bambu_last_error ? gst_bambu_last_error : 2;
PostGtkSinkStateEvent(GetId());
}
return;
}
if (!m_imp) {
m_error = m_native_wayland && !m_gtk_sink_error.empty() ? 104 : 100;
m_loaded = false;
if (m_native_wayland && !m_gtk_sink_error.empty() && !m_gtk_sink_error_notified) {
m_gtk_sink_error_notified = true;
const wxString message = m_gtk_sink_error;
CallAfter([message] {
wxMessageBox(message, _L("Error"), wxOK);
});
}
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(GetId());
event.SetEventObject(this);
wxPostEvent(this, event);
return;
}
#endif
wxMediaCtrl::Load(url);
}
void wxMediaCtrl2::Play() { wxMediaCtrl::Play(); }
void wxMediaCtrl2::Play()
{
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_use_gtk_sink && m_gtk_playbin) {
GstStateChangeReturn state = gst_element_set_state(m_gtk_playbin, GST_STATE_PLAYING);
if (state == GST_STATE_CHANGE_FAILURE) {
m_error = gst_bambu_last_error ? gst_bambu_last_error : 2;
m_gtk_state = wxMEDIASTATE_STOPPED;
PostGtkSinkStateEvent(GetId());
}
return;
}
if (!m_imp) {
m_error = m_native_wayland && !m_gtk_sink_error.empty() ? 104 : 100;
if (m_native_wayland && !m_gtk_sink_error.empty() && !m_gtk_sink_error_notified) {
m_gtk_sink_error_notified = true;
const wxString message = m_gtk_sink_error;
CallAfter([message] {
wxMessageBox(message, _L("Error"), wxOK);
});
}
PostGtkSinkStateEvent(GetId());
return;
}
#endif
wxMediaCtrl::Play();
}
void wxMediaCtrl2::Stop()
{
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_use_gtk_sink && m_gtk_playbin) {
gst_element_set_state(m_gtk_playbin, GST_STATE_NULL);
m_gtk_state = wxMEDIASTATE_STOPPED;
m_loaded = false;
PostGtkSinkStateEvent(0);
return;
}
if (!m_imp)
return;
#endif
wxMediaCtrl::Stop();
}
#ifdef __LINUX__
extern "C" int gst_bambu_last_error;
wxMediaState wxMediaCtrl2::GetState()
{
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_use_gtk_sink && m_gtk_playbin)
return m_gtk_state;
if (!m_imp)
return wxMEDIASTATE_STOPPED;
#endif
return wxMediaCtrl::GetState();
}
int wxMediaCtrl2::GetLastError() const
{
#ifdef __LINUX__
#ifdef __WXGTK__
if (m_use_gtk_sink && m_error)
return m_error;
#endif
if (m_error)
return m_error;
return gst_bambu_last_error;
#else
return m_error;

View File

@@ -13,6 +13,10 @@
wxDECLARE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
#if defined(__LINUX__) && defined(__WXGTK__)
typedef struct _GstElement GstElement;
#endif
#ifdef __WXMAC__
class wxMediaCtrl2 : public wxWindow
@@ -59,6 +63,7 @@ class wxMediaCtrl2 : public wxMediaCtrl
{
public:
wxMediaCtrl2(wxWindow *parent);
~wxMediaCtrl2();
void Load(wxURI url);
@@ -68,6 +73,8 @@ public:
void SetIdleImage(wxString const & image);
wxMediaState GetState();
int GetLastError() const;
wxSize GetVideoSize() const;
@@ -84,6 +91,21 @@ protected:
#endif
private:
#if defined(__LINUX__) && defined(__WXGTK__)
bool CreateGtkSinkPlayer();
void DestroyGtkSinkPlayer();
void PostGtkSinkStateEvent(int id = 0);
bool m_native_wayland = false;
bool m_use_gtk_sink = false;
wxString m_gtk_sink_error;
bool m_gtk_sink_error_notified = false;
GstElement *m_gtk_playbin = nullptr;
GstElement *m_gtk_sink = nullptr;
unsigned int m_gtk_bus_watch_id = 0;
wxWindow *m_gtk_video_window = nullptr;
wxMediaState m_gtk_state = wxMEDIASTATE_STOPPED;
#endif
wxString m_idle_image;
int m_error = 0;
bool m_loaded = false;