Commit 531a7e72 authored by Jan Grulich's avatar Jan Grulich
Browse files

Move Wayland integration bits into separated class so they can be shared between portals

parent cd513b72
......@@ -28,7 +28,8 @@ if (SCREENCAST_ENABLED)
screencaststream.cpp
screenchooserdialog.cpp
remotedesktop.cpp
remotedesktopdialog.cpp)
remotedesktopdialog.cpp
waylandintegration.cpp)
ki18n_wrap_ui(xdg_desktop_portal_kde_SRCS
screenchooserdialog.ui
......
......@@ -43,6 +43,9 @@ DesktopPortal::DesktopPortal(QObject *parent)
#endif
, m_screenshot(new ScreenshotPortal(this))
{
#if SCREENCAST_ENABLED
WaylandIntegration::init();
#endif
}
DesktopPortal::~DesktopPortal()
......
......@@ -34,6 +34,7 @@
#if SCREENCAST_ENABLED
#include "screencast.h"
#include "remotedesktop.h"
#include "waylandintegration.h"
#endif
#include "screenshot.h"
......
......@@ -22,27 +22,16 @@
#include "session.h"
#include "screencaststream.h"
#include "screenchooserdialog.h"
#include <QEventLoop>
#include <QLoggingCategory>
#include <QThread>
#include <QTimer>
#include "waylandintegration.h"
#include <QDBusMetaType>
#include <QDBusError>
#include <QDBusArgument>
#include <QDBusConnection>
// KWayland
#include <KWayland/Client/connection_thread.h>
#include <KWayland/Client/event_queue.h>
#include <KWayland/Client/registry.h>
#include <KWayland/Client/output.h>
#include <KWayland/Client/remote_access.h>
// system
#include <fcntl.h>
#include <unistd.h>
#include <QEventLoop>
#include <QLoggingCategory>
#include <QTimer>
Q_LOGGING_CATEGORY(XdgDesktopPortalKdeScreenCast, "xdp-kde-screencast")
......@@ -79,80 +68,16 @@ const QDBusArgument &operator << (QDBusArgument &arg, const ScreenCastPortal::St
return arg;
}
static const char * formatGLError(GLenum err)
{
switch(err) {
case GL_NO_ERROR:
return "GL_NO_ERROR";
case GL_INVALID_ENUM:
return "GL_INVALID_ENUM";
case GL_INVALID_VALUE:
return "GL_INVALID_VALUE";
case GL_INVALID_OPERATION:
return "GL_INVALID_OPERATION";
case GL_STACK_OVERFLOW:
return "GL_STACK_OVERFLOW";
case GL_STACK_UNDERFLOW:
return "GL_STACK_UNDERFLOW";
case GL_OUT_OF_MEMORY:
return "GL_OUT_OF_MEMORY";
default:
return (QLatin1String("0x") + QString::number(err, 16)).toLocal8Bit().constData();
}
}
// Thank you kscreen
void ScreenCastPortalOutput::setOutputType(const QString &type)
{
const auto embedded = { QLatin1String("LVDS"),
QLatin1String("IDP"),
QLatin1String("EDP"),
QLatin1String("LCD") };
for (const QLatin1String &pre : embedded) {
if (type.toUpper().startsWith(pre)) {
outputType = OutputType::Laptop;
return;
}
}
if (type.contains("VGA") || type.contains("DVI") || type.contains("HDMI") || type.contains("Panel") ||
type.contains("DisplayPort") || type.startsWith("DP") || type.contains("unknown")) {
outputType = OutputType::Monitor;
} else if (type.contains("TV")) {
outputType = OutputType::Television;
} else {
outputType = OutputType::Monitor;
}
}
ScreenCastPortal::ScreenCastPortal(QObject *parent)
: QDBusAbstractAdaptor(parent)
, m_registryInitialized(false)
, m_streamingEnabled(false)
, m_connection(nullptr)
, m_queue(nullptr)
, m_registry(nullptr)
, m_remoteAccessManager(nullptr)
{
initDrm();
initEGL();
initWayland();
qDBusRegisterMetaType<ScreenCastPortal::Stream>();
qDBusRegisterMetaType<ScreenCastPortal::Streams>();
}
ScreenCastPortal::~ScreenCastPortal()
{
if (m_remoteAccessManager) {
m_remoteAccessManager->destroy();
}
if (m_drmFd) {
gbm_device_destroy(m_gbmDevice);
}
m_stream->deleteLater();
}
......@@ -165,123 +90,17 @@ void ScreenCastPortal::createPipeWireStream(const QSize &resolution)
qCDebug(XdgDesktopPortalKdeScreenCast) << "Pipewire stream is ready: " << nodeId;
});
connect(WaylandIntegration::waylandIntegration(), &WaylandIntegration::WaylandIntegration::newBuffer, m_stream, &ScreenCastStream::recordFrame);
connect(m_stream, &ScreenCastStream::startStreaming, this, [this] {
qCDebug(XdgDesktopPortalKdeScreenCast) << "Start streaming";
m_streamingEnabled = true;
if (!m_registryInitialized) {
qCWarning(XdgDesktopPortalKdeScreenCast) << "Cannot start stream because registry is not initialized yet";
return;
}
if (m_registry->hasInterface(KWayland::Client::Registry::Interface::RemoteAccessManager)) {
KWayland::Client::Registry::AnnouncedInterface interface = m_registry->interface(KWayland::Client::Registry::Interface::RemoteAccessManager);
if (!interface.name && !interface.version) {
qCWarning(XdgDesktopPortalKdeScreenCast) << "Cannot start stream because remote access interface is not initialized yet";
return;
}
m_remoteAccessManager = m_registry->createRemoteAccessManager(interface.name, interface.version);
connect(m_remoteAccessManager, &KWayland::Client::RemoteAccessManager::bufferReady, this, [this] (const void *output, const KWayland::Client::RemoteBuffer * rbuf) {
Q_UNUSED(output);
connect(rbuf, &KWayland::Client::RemoteBuffer::parametersObtained, this, [this, rbuf] {
processBuffer(rbuf);
});
});
}
WaylandIntegration::startStreaming();
});
connect(m_stream, &ScreenCastStream::stopStreaming, this, &ScreenCastPortal::stopStreaming);
}
void ScreenCastPortal::initDrm()
{
m_drmFd = open("/dev/dri/renderD128", O_RDWR);
m_gbmDevice = gbm_create_device(m_drmFd);
if (!m_gbmDevice) {
qFatal("Cannot create GBM device: %s", strerror(errno));
}
}
void ScreenCastPortal::initEGL()
{
// Get the list of client extensions
const char* clientExtensionsCString = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
const QByteArray clientExtensionsString = QByteArray::fromRawData(clientExtensionsCString, qstrlen(clientExtensionsCString));
if (clientExtensionsString.isEmpty()) {
// If eglQueryString() returned NULL, the implementation doesn't support
// EGL_EXT_client_extensions. Expect an EGL_BAD_DISPLAY error.
qFatal("No client extensions defined! %s", formatGLError(eglGetError()));
}
m_egl.extensions = clientExtensionsString.split(' ');
// Use eglGetPlatformDisplayEXT() to get the display pointer
// if the implementation supports it.
if (!m_egl.extensions.contains(QByteArrayLiteral("EGL_EXT_platform_base")) ||
!m_egl.extensions.contains(QByteArrayLiteral("EGL_MESA_platform_gbm"))) {
qFatal("One of required EGL extensions is missing");
}
m_egl.display = eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_MESA, m_gbmDevice, nullptr);
if (m_egl.display == EGL_NO_DISPLAY) {
qFatal("Error during obtaining EGL display: %s", formatGLError(eglGetError()));
}
EGLint major, minor;
if (eglInitialize(m_egl.display, &major, &minor) == EGL_FALSE) {
qFatal("Error during eglInitialize: %s", formatGLError(eglGetError()));
}
if (eglBindAPI(EGL_OPENGL_API) == EGL_FALSE) {
qFatal("bind OpenGL API failed");
}
m_egl.context = eglCreateContext(m_egl.display, nullptr, EGL_NO_CONTEXT, nullptr);
if (m_egl.context == EGL_NO_CONTEXT) {
qFatal("Couldn't create EGL context: %s", formatGLError(eglGetError()));
}
qCDebug(XdgDesktopPortalKdeScreenCast) << "Egl initialization succeeded";
qCDebug(XdgDesktopPortalKdeScreenCast) << QString("EGL version: %1.%2").arg(major).arg(minor);
}
void ScreenCastPortal::initWayland()
{
m_thread = new QThread(this);
m_connection = new KWayland::Client::ConnectionThread;
connect(m_connection, &KWayland::Client::ConnectionThread::connected, this, &ScreenCastPortal::setupRegistry, Qt::QueuedConnection);
connect(m_connection, &KWayland::Client::ConnectionThread::connectionDied, this, [this] {
if (m_queue) {
delete m_queue;
m_queue = nullptr;
}
m_connection->deleteLater();
m_connection = nullptr;
if (m_thread) {
m_thread->quit();
if (!m_thread->wait(3000)) {
m_thread->terminate();
m_thread->wait();
}
delete m_thread;
m_thread = nullptr;
}
});
connect(m_connection, &KWayland::Client::ConnectionThread::failed, this, [this] {
m_thread->quit();
m_thread->wait();
});
m_thread->start();
m_connection->moveToThread(m_thread);
m_connection->initConnection();
}
uint ScreenCastPortal::CreateSession(const QDBusObjectPath &handle,
const QDBusObjectPath &session_handle,
const QString &app_id,
......@@ -372,18 +191,18 @@ uint ScreenCastPortal::Start(const QDBusObjectPath &handle,
}
// TODO check whether we got some outputs?
if (m_outputMap.isEmpty()) {
if (WaylandIntegration::screens().isEmpty()) {
qCWarning(XdgDesktopPortalKdeScreenCast) << "Failed to show dialog as there is no screen to select";
return 2;
}
QScopedPointer<ScreenChooserDialog, QScopedPointerDeleteLater> screenDialog(new ScreenChooserDialog(m_outputMap, session->multipleSources()));
QScopedPointer<ScreenChooserDialog, QScopedPointerDeleteLater> screenDialog(new ScreenChooserDialog(session->multipleSources()));
if (screenDialog->exec()) {
ScreenCastPortalOutput selectedOutput = m_outputMap.value(screenDialog->selectedScreens().first());
WaylandIntegration::WaylandOutput selectedOutput = WaylandIntegration::screens().value(screenDialog->selectedScreens().first());
// Initialize PipeWire
createPipeWireStream(selectedOutput.resolution);
createPipeWireStream(selectedOutput.resolution());
// HACK wait for stream to be ready
bool streamReady = false;
......@@ -407,13 +226,11 @@ uint ScreenCastPortal::Start(const QDBusObjectPath &handle,
qCDebug(XdgDesktopPortalKdeScreenCast) << "Pipewire node id: " << m_stream->nodeId();
KWayland::Client::Output *output = new KWayland::Client::Output(this);
output->setup(m_registry->bindOutput(selectedOutput.waylandOutputName, selectedOutput.waylandOutputVersion));
m_bindOutputs << output;
WaylandIntegration::bindOutput(selectedOutput.waylandOutputName(), selectedOutput.waylandOutputVersion());
Stream stream;
stream.nodeId = m_stream->nodeId();
stream.map = QVariantMap({{QLatin1String("size"), selectedOutput.resolution}});
stream.map = QVariantMap({{QLatin1String("size"), selectedOutput.resolution()}});
results.insert(QLatin1String("streams"), QVariant::fromValue<ScreenCastPortal::Streams>({stream}));
return 0;
......@@ -422,149 +239,12 @@ uint ScreenCastPortal::Start(const QDBusObjectPath &handle,
return 0;
}
void ScreenCastPortal::addOutput(quint32 name, quint32 version)
{
KWayland::Client::Output *output = new KWayland::Client::Output(this);
output->setup(m_registry->bindOutput(name, version));
connect(output, &KWayland::Client::Output::changed, this, [this, name, version, output] () {
qCDebug(XdgDesktopPortalKdeScreenCast) << "Adding output:";
qCDebug(XdgDesktopPortalKdeScreenCast) << " manufacturer: " << output->manufacturer();
qCDebug(XdgDesktopPortalKdeScreenCast) << " model: " << output->model();
qCDebug(XdgDesktopPortalKdeScreenCast) << " resolution: " << output->pixelSize();
ScreenCastPortalOutput portalOutput;
portalOutput.manufacturer = output->manufacturer();
portalOutput.model = output->model();
portalOutput.resolution = output->pixelSize();
portalOutput.waylandOutputName = name;
portalOutput.waylandOutputVersion = version;
portalOutput.setOutputType(output->model());
m_outputMap.insert(name, portalOutput);
delete output;
});
}
void ScreenCastPortal::removeOutput(quint32 name)
{
ScreenCastPortalOutput output = m_outputMap.take(name);
qCDebug(XdgDesktopPortalKdeScreenCast) << "Removing output:";
qCDebug(XdgDesktopPortalKdeScreenCast) << " manufacturer: " << output.manufacturer;
qCDebug(XdgDesktopPortalKdeScreenCast) << " model: " << output.model;
}
void ScreenCastPortal::processBuffer(const KWayland::Client::RemoteBuffer* rbuf)
{
QScopedPointer<const KWayland::Client::RemoteBuffer> guard(rbuf);
auto gbmHandle = rbuf->fd();
auto width = rbuf->width();
auto height = rbuf->height();
auto stride = rbuf->stride();
auto format = rbuf->format();
qCDebug(XdgDesktopPortalKdeScreenCast) << QString("Incoming GBM fd %1, %2x%3, stride %4, fourcc 0x%5").arg(gbmHandle).arg(width).arg(height).arg(stride).arg(QString::number(format, 16));
if (!m_streamingEnabled) {
qCDebug(XdgDesktopPortalKdeScreenCast) << "Streaming is disabled";
close(gbmHandle);
return;
}
if (!gbm_device_is_format_supported(m_gbmDevice, format, GBM_BO_USE_SCANOUT)) {
qCritical() << "GBM format is not supported by device!";
}
// import GBM buffer that was passed from KWin
gbm_import_fd_data importInfo = {gbmHandle, width, height, stride, format};
gbm_bo *imported = gbm_bo_import(m_gbmDevice, GBM_BO_IMPORT_FD, &importInfo, GBM_BO_USE_SCANOUT);
if (!imported) {
qCritical() << "Cannot import passed GBM fd:" << strerror(errno);
}
// bind context to render thread
eglMakeCurrent(m_egl.display, EGL_NO_SURFACE, EGL_NO_SURFACE, m_egl.context);
// create EGL image from imported BO
EGLImageKHR image = eglCreateImageKHR(m_egl.display, nullptr, EGL_NATIVE_PIXMAP_KHR, imported, nullptr);
if (image == EGL_NO_IMAGE_KHR) {
qCritical() << "Error creating EGLImageKHR" << formatGLError(glGetError());
return;
}
// create GL 2D texture for framebuffer
GLuint texture;
glGenTextures(1, &texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, texture);
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
// bind framebuffer to copy pixels from
GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
qCritical() << "glCheckFramebufferStatus failed:" << formatGLError(glGetError());
glDeleteTextures(1, &texture);
glDeleteFramebuffers(1, &framebuffer);
eglDestroyImageKHR(m_egl.display, image);
return;
}
auto capture = new QImage(QSize(width, height), QImage::Format_RGBA8888);
glViewport(0, 0, width, height);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, capture->bits());
m_stream->recordFrame(capture->bits());
gbm_bo_destroy(imported);
glDeleteTextures(1, &texture);
glDeleteFramebuffers(1, &framebuffer);
eglDestroyImageKHR(m_egl.display, image);
delete capture;
close(gbmHandle);
}
void ScreenCastPortal::setupRegistry()
{
m_queue = new KWayland::Client::EventQueue(this);
m_queue->setup(m_connection);
m_registry = new KWayland::Client::Registry(this);
connect(m_registry, &KWayland::Client::Registry::outputAnnounced, this, &ScreenCastPortal::addOutput);
connect(m_registry, &KWayland::Client::Registry::outputRemoved, this, &ScreenCastPortal::removeOutput);
connect(m_registry, &KWayland::Client::Registry::interfacesAnnounced, this, [this] {
m_registryInitialized = true;
qCDebug(XdgDesktopPortalKdeScreenCast) << "Registry initialized";
});
m_registry->create(m_connection);
m_registry->setEventQueue(m_queue);
m_registry->setup();
}
void ScreenCastPortal::stopStreaming()
{
if (m_streamingEnabled) {
qCDebug(XdgDesktopPortalKdeScreenCast) << "Stop streaming";
m_remoteAccessManager->release();
m_remoteAccessManager->destroy();
WaylandIntegration::stopStreaming();
m_streamingEnabled = false;
qDeleteAll(m_bindOutputs);
m_bindOutputs.clear();
delete m_stream;
m_stream = nullptr;
}
......
......@@ -25,56 +25,15 @@
#include <QDBusObjectPath>
#include <QSize>
#include <gbm.h>
#include <epoxy/egl.h>
#include <epoxy/gl.h>
namespace KWayland {
namespace Client {
class ConnectionThread;
class EventQueue;
class OutputDevice;
class Registry;
class RemoteAccessManager;
class RemoteBuffer;
class Output;
}
}
class ScreenChooserDialog;
class ScreenCastStream;
class ScreenCastPortalOutput
{
enum OutputType {
Laptop,
Monitor,
Television
};
void setOutputType(const QString &type);
QString manufacturer;
QString model;
QSize resolution;
OutputType outputType;
// Needed for later output binding
int waylandOutputName;
int waylandOutputVersion;
friend class ScreenCastPortal;
friend class ScreenChooserDialog;
};
class ScreenCastPortal : public QDBusAbstractAdaptor
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.freedesktop.impl.portal.ScreenCast")
Q_PROPERTY(uint version READ version)
Q_PROPERTY(uint AvailableSourceTypes READ AvailableSourceTypes)
public:
typedef struct {
uint nodeId;
......@@ -115,40 +74,14 @@ public Q_SLOTS:
QVariantMap &results);
private Q_SLOTS:
void addOutput(quint32 name, quint32 version);
void removeOutput(quint32 name);
void processBuffer(const KWayland::Client::RemoteBuffer *rbuf);
void setupRegistry();
void stopStreaming();
private:
void createPipeWireStream(const QSize &resolution);
void initDrm();
void initEGL();
void initWayland();
bool m_registryInitialized;
bool m_streamingEnabled;
QMap<quint32, ScreenCastPortalOutput> m_outputMap;
QList<KWayland::Client::Output*> m_bindOutputs;
QThread *m_thread;
ScreenCastStream *m_stream;
KWayland::Client::ConnectionThread *m_connection;
KWayland::Client::EventQueue *m_queue;
KWayland::Client::Registry *m_registry;
KWayland::Client::RemoteAccessManager *m_remoteAccessManager;
qint32 m_drmFd = 0; // for GBM buffer mmap
gbm_device *m_gbmDevice = nullptr; // for passed GBM buffer retrieval
struct {
QList<QByteArray> extensions;
EGLDisplay display = EGL_NO_DISPLAY;
EGLContext context = EGL_NO_CONTEXT;
} m_egl;
bool m_streamingEnabled;
};
#endif // XDG_DESKTOP_PORTAL_KDE_SCREENCAST_H
......
......@@ -70,9 +70,11 @@ public:
// Public because we need access from static functions
bool createStream();
void removeStream();
public Q_SLOTS:
bool recordFrame(uint8_t *screenData);
void removeStream();
Q_SIGNALS:
void streamReady(uint nodeId);
void startStreaming();
......
......@@ -21,6 +21,7 @@
#include "screenchooserdialog.h"
#include "ui_screenchooserdialog.h"
#include "screencast.h"
#include "waylandintegration.h"
#include <QLoggingCategory>
#include <QSettings>
......@@ -29,26 +30,26 @@
Q_LOGGING_CATEGORY(XdgDesktopPortalKdeScreenChooserDialog, "xdp-kde-screen-chooser-dialog")
ScreenChooserDialog::ScreenChooserDialog(const QMap<quint32, ScreenCastPortalOutput> &screens, bool multiple, QDialog *parent, Qt::WindowFlags flags)
ScreenChooserDialog::ScreenChooserDialog(bool multiple, QDialog *parent, Qt::WindowFlags flags)
: QDialog(parent, flags)
, m_dialog(new Ui::ScreenChooserDialog)
{
m_dialog->setupUi(this);
QMapIterator<quint32, ScreenCastPortalOutput> it(screens);
QMapIterator<quint32, WaylandIntegration::WaylandOutput> it(WaylandIntegration::screens());
while (it.hasNext()) {
it.next();
QListWidgetItem *widgetItem = new QListWidgetItem(m_dialog->screenView);
widgetItem->setData(Qt::UserRole, it.key());
if (it.value().outputType == ScreenCastPortalOutput::Laptop) {
if (it.value().outputType() == WaylandIntegration::WaylandOutput::Laptop) {
widgetItem->setIcon(QIcon::fromTheme("computer-laptop"));
widgetItem->setText(i18n("Laptop screen\nModel: %1", it.value().model));
} else if (it.value().outputType == ScreenCastPortalOutput::Monitor) {
widgetItem->setText(i18n("Laptop screen\nModel: %1", it.value().model()));
} else if (it.value().outputType() == WaylandIntegration::WaylandOutput::Monitor) {
widgetItem->setIcon(QIcon::fromTheme("video-display"));
widgetItem->setText(i18n("Manufacturer: %1\nModel: %2", it.value().manufacturer, it.value().model));
widgetItem->setText(i18n("Manufacturer: %1\nModel: %2", it.value().manufacturer(), it.value().model()));
} else {
widgetItem->setIcon(QIcon::fromTheme("video-television"));
widgetItem->setText(i18n("Manufacturer: %1\nModel: %2", it.value().manufacturer, it.value().model));
widgetItem->setText(i18n("Manufacturer: %1\nModel: %2", it.value().manufacturer(), it.value().model()));
}
}
......
......@@ -29,19 +29,17 @@ namespace Ui
class ScreenChooserDialog;
}