diff --git a/src/screencast.cpp b/src/screencast.cpp index 4e3b1a1..14887e9 100644 --- a/src/screencast.cpp +++ b/src/screencast.cpp @@ -1,579 +1,578 @@ /* * Copyright © 2018 Red Hat, Inc * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "screencast.h" #include "session.h" #include "screencaststream.h" #include "screenchooserdialog.h" #include #include #include #include #include #include #include // KWayland #include #include #include #include #include // system #include #include Q_LOGGING_CATEGORY(XdgDesktopPortalKdeScreenCast, "xdp-kde-screencast") Q_DECLARE_METATYPE(ScreenCastPortal::Stream); Q_DECLARE_METATYPE(ScreenCastPortal::Streams); const QDBusArgument &operator >> (const QDBusArgument &arg, ScreenCastPortal::Stream &stream) { arg.beginStructure(); arg >> stream.nodeId; arg.beginMap(); while (!arg.atEnd()) { QString key; QVariant map; arg.beginMapEntry(); arg >> key >> map; arg.endMapEntry(); stream.map.insert(key, map); } arg.endMap(); arg.endStructure(); return arg; } const QDBusArgument &operator << (QDBusArgument &arg, const ScreenCastPortal::Stream &stream) { arg.beginStructure(); arg << stream.nodeId; arg << stream.map; arg.endStructure(); 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(); - initPipewire(); initWayland(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); } ScreenCastPortal::~ScreenCastPortal() { if (m_remoteAccessManager) { m_remoteAccessManager->destroy(); } if (m_drmFd) { gbm_device_destroy(m_gbmDevice); } m_stream->deleteLater(); } +void ScreenCastPortal::createPipeWireStream(const QSize &resolution) +{ + m_stream = new ScreenCastStream(resolution); + m_stream->init(); + + connect(m_stream, &ScreenCastStream::streamReady, this, [] (uint nodeId) { + qCDebug(XdgDesktopPortalKdeScreenCast) << "Pipewire stream is ready: " << nodeId; + }); + + 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); + }); + }); + } + }); + + 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::initPipewire() -{ - m_stream = new ScreenCastStream; - m_stream->init(); - - connect(m_stream, &ScreenCastStream::streamReady, this, [] (uint nodeId) { - qCDebug(XdgDesktopPortalKdeScreenCast) << "Pipewire stream is ready: " << nodeId; - }); - - 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); - }); - }); - } - }); - - connect(m_stream, &ScreenCastStream::stopStreaming, this, &ScreenCastPortal::stopStreaming); -} - 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, const QVariantMap &options, QVariantMap &results) { Q_UNUSED(results) qCDebug(XdgDesktopPortalKdeScreenCast) << "CreateSession called with parameters:"; qCDebug(XdgDesktopPortalKdeScreenCast) << " handle: " << handle.path(); qCDebug(XdgDesktopPortalKdeScreenCast) << " session_handle: " << session_handle.path(); qCDebug(XdgDesktopPortalKdeScreenCast) << " app_id: " << app_id; qCDebug(XdgDesktopPortalKdeScreenCast) << " options: " << options; QDBusConnection sessionBus = QDBusConnection::sessionBus(); Session *session = new Session(this, app_id, session_handle.path()); if (sessionBus.registerVirtualObject(session_handle.path(), session, QDBusConnection::VirtualObjectRegisterOption::SubPath)) { connect(session, &Session::closed, [this, session, session_handle] () { m_sessionList.remove(session_handle.path()); QDBusConnection::sessionBus().unregisterObject(session_handle.path()); session->deleteLater(); stopStreaming(); }); m_sessionList.insert(session_handle.path(), session); return 0; } else { qCDebug(XdgDesktopPortalKdeScreenCast) << sessionBus.lastError().message(); qCDebug(XdgDesktopPortalKdeScreenCast) << "Failed to register session object: " << session_handle.path(); session->deleteLater(); return 2; } } uint ScreenCastPortal::SelectSources(const QDBusObjectPath &handle, const QDBusObjectPath &session_handle, const QString &app_id, const QVariantMap &options, QVariantMap &results) { Q_UNUSED(results) qCDebug(XdgDesktopPortalKdeScreenCast) << "SelectSource called with parameters:"; qCDebug(XdgDesktopPortalKdeScreenCast) << " handle: " << handle.path(); qCDebug(XdgDesktopPortalKdeScreenCast) << " session_handle: " << session_handle.path(); qCDebug(XdgDesktopPortalKdeScreenCast) << " app_id: " << app_id; qCDebug(XdgDesktopPortalKdeScreenCast) << " options: " << options; uint types = Monitor; Session *session = nullptr; session = m_sessionList.value(session_handle.path()); if (!session) { qCWarning(XdgDesktopPortalKdeScreenCast) << "Tried to select sources on non-existing session " << session_handle.path(); return 2; } if (options.contains(QLatin1String("multiple"))) { session->setMultipleSources(options.value(QLatin1String("multiple")).toBool()); } if (options.contains(QLatin1String("types"))) { types = (SourceType)(options.value(QLatin1String("types")).toUInt()); } if (types == Window) { qCWarning(XdgDesktopPortalKdeScreenCast) << "Screen cast of a window is not implemented"; return 2; } return 0; } uint ScreenCastPortal::Start(const QDBusObjectPath &handle, const QDBusObjectPath &session_handle, const QString &app_id, const QString &parent_window, const QVariantMap &options, QVariantMap &results) { Q_UNUSED(results) qCDebug(XdgDesktopPortalKdeScreenCast) << "Start called with parameters:"; qCDebug(XdgDesktopPortalKdeScreenCast) << " handle: " << handle.path(); qCDebug(XdgDesktopPortalKdeScreenCast) << " session_handle: " << session_handle.path(); qCDebug(XdgDesktopPortalKdeScreenCast) << " app_id: " << app_id; qCDebug(XdgDesktopPortalKdeScreenCast) << " parent_window: " << parent_window; qCDebug(XdgDesktopPortalKdeScreenCast) << " options: " << options; Session *session = nullptr; session = m_sessionList.value(session_handle.path()); if (!session) { qCWarning(XdgDesktopPortalKdeScreenCast) << "Tried to select sources on non-existing session " << session_handle.path(); return 2; } // TODO check whether we got some outputs? if (m_outputMap.isEmpty()) { qCWarning(XdgDesktopPortalKdeScreenCast) << "Failed to show dialog as there is no screen to select"; return 2; } QScopedPointer screenDialog(new ScreenChooserDialog(m_outputMap, session->multipleSources())); if (screenDialog->exec()) { ScreenCastPortalOutput selectedOutput = m_outputMap.value(screenDialog->selectedScreens().first()); + // Initialize PipeWire + createPipeWireStream(selectedOutput.resolution); + // HACK wait for stream to be ready bool streamReady = false; QEventLoop loop; connect(m_stream, &ScreenCastStream::streamReady, this, [&loop, &streamReady] { loop.quit(); streamReady = true; }); - if (!m_stream->createStream(selectedOutput.resolution)) { - qCWarning(XdgDesktopPortalKdeScreenCast) << "Failed to create pipewire stream"; - return 2; - } - QTimer::singleShot(3000, &loop, &QEventLoop::quit); loop.exec(); disconnect(m_stream, &ScreenCastStream::streamReady, this, nullptr); if (!streamReady) { qCWarning(XdgDesktopPortalKdeScreenCast) << "Pipewire stream is not ready to be streamed"; return 2; } // TODO support multiple outputs 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; Stream stream; stream.nodeId = m_stream->nodeId(); stream.map = QVariantMap({{QLatin1String("size"), selectedOutput.resolution}}); results.insert(QLatin1String("streams"), QVariant::fromValue({stream})); return 0; } 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 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, NULL, EGL_NATIVE_PIXMAP_KHR, imported, NULL); 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(); m_streamingEnabled = false; - m_stream->removeStream(); qDeleteAll(m_bindOutputs); m_bindOutputs.clear(); + + delete m_stream; + m_stream = nullptr; } } diff --git a/src/screencast.h b/src/screencast.h index a7c8cc4..50c7240 100644 --- a/src/screencast.h +++ b/src/screencast.h @@ -1,157 +1,157 @@ /* * Copyright © 2018 Red Hat, Inc * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #ifndef XDG_DESKTOP_PORTAL_KDE_SCREENCAST_H #define XDG_DESKTOP_PORTAL_KDE_SCREENCAST_H #include #include #include #include #include #include namespace KWayland { namespace Client { class ConnectionThread; class EventQueue; class OutputDevice; class Registry; class RemoteAccessManager; class RemoteBuffer; class Output; } } class Session; 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; QVariantMap map; } Stream; typedef QList Streams; enum SourceType { Any = 0, Monitor, Window }; ScreenCastPortal(QObject *parent); ~ScreenCastPortal(); uint version() const { return 1; } uint AvailableSourceTypes() const { return Monitor; }; public Q_SLOTS: uint CreateSession(const QDBusObjectPath &handle, const QDBusObjectPath &session_handle, const QString &app_id, const QVariantMap &options, QVariantMap &results); uint SelectSources(const QDBusObjectPath &handle, const QDBusObjectPath &session_handle, const QString &app_id, const QVariantMap &options, QVariantMap &results); uint Start(const QDBusObjectPath &handle, const QDBusObjectPath &session_handle, const QString &app_id, const QString &parent_window, const QVariantMap &options, 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 initPipewire(); void initWayland(); bool m_registryInitialized; bool m_streamingEnabled; QMap m_sessionList; QMap m_outputMap; QList 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 extensions; EGLDisplay display = EGL_NO_DISPLAY; EGLContext context = EGL_NO_CONTEXT; } m_egl; }; #endif // XDG_DESKTOP_PORTAL_KDE_SCREENCAST_H diff --git a/src/screencaststream.cpp b/src/screencaststream.cpp index aa09e4c..cb22ab4 100644 --- a/src/screencaststream.cpp +++ b/src/screencaststream.cpp @@ -1,433 +1,438 @@ /* * Copyright © 2018 Red Hat, Inc * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "screencaststream.h" #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(XdgDesktopPortalKdeScreenCastStream, "xdp-kde-screencast-stream") class PwFraction { public: int num; int denom; }; // Stolen from mutter #define MAX_TERMS 30 #define MIN_DIVISOR 1.0e-10 #define MAX_ERROR 1.0e-20 #define PROP_RANGE(min, max) 2, (min), (max) #define BITS_PER_PIXEL 4 static int greatestCommonDivisor(int a, int b) { while (b != 0) { int temp = a; a = b; b = temp % b; } return ABS(a); } static PwFraction pipewireFractionFromDouble(double src) { double V, F; /* double being converted */ int N, D; /* will contain the result */ int A; /* current term in continued fraction */ int64_t N1, D1; /* numerator, denominator of last approx */ int64_t N2, D2; /* numerator, denominator of previous approx */ int i; int gcd; gboolean negative = FALSE; /* initialize fraction being converted */ F = src; if (F < 0.0) { F = -F; negative = TRUE; } V = F; /* initialize fractions with 1/0, 0/1 */ N1 = 1; D1 = 0; N2 = 0; D2 = 1; N = 1; D = 1; for (i = 0; i < MAX_TERMS; i++) { /* get next term */ A = (gint) F; /* no floor() needed, F is always >= 0 */ /* get new divisor */ F = F - A; /* calculate new fraction in temp */ N2 = N1 * A + N2; D2 = D1 * A + D2; /* guard against overflow */ if (N2 > G_MAXINT || D2 > G_MAXINT) break; N = N2; D = D2; /* save last two fractions */ N2 = N1; D2 = D1; N1 = N; D1 = D; /* quit if dividing by zero or close enough to target */ if (F < MIN_DIVISOR || fabs (V - ((gdouble) N) / D) < MAX_ERROR) break; /* Take reciprocal */ F = 1 / F; } /* fix for overflow */ if (D == 0) { N = G_MAXINT; D = 1; } /* fix for negative */ if (negative) N = -N; /* simplify */ gcd = greatestCommonDivisor(N, D); if (gcd) { N /= gcd; D /= gcd; } PwFraction fraction; fraction.num = N; fraction.denom = D; return fraction; } -static void onStateChanged(void *_data, pw_remote_state old, pw_remote_state state, const char *error) +static void onStateChanged(void *data, pw_remote_state old, pw_remote_state state, const char *error) { Q_UNUSED(old); - Q_UNUSED(_data); + + ScreenCastStream *pw = static_cast(data); switch (state) { case PW_REMOTE_STATE_ERROR: // TODO notify error qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Remote error: " << error; break; case PW_REMOTE_STATE_CONNECTED: // TODO notify error qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Remote state: " << pw_remote_state_as_string(state); + if (!pw->createStream()) { + pw->stopStreaming(); + } break; default: qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Remote state: " << pw_remote_state_as_string(state); break; } } static void onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message) { Q_UNUSED(old) ScreenCastStream *pw = static_cast(data); switch (state) { case PW_STREAM_STATE_ERROR: qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Stream error: " << error_message; break; case PW_STREAM_STATE_CONFIGURE: qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); Q_EMIT pw->streamReady((uint)pw_stream_get_node_id(pw->pwStream)); break; case PW_STREAM_STATE_UNCONNECTED: case PW_STREAM_STATE_CONNECTING: case PW_STREAM_STATE_READY: case PW_STREAM_STATE_PAUSED: qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); Q_EMIT pw->stopStreaming(); break; case PW_STREAM_STATE_STREAMING: qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); Q_EMIT pw->startStreaming(); break; } } static void onStreamFormatChanged(void *data, struct spa_pod *format) { qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream format changed"; ScreenCastStream *pw = static_cast(data); uint8_t paramsBuffer[1024]; int32_t width, height, stride, size; struct spa_pod_builder pod_builder; struct spa_pod *params[1]; const int bpp = 4; if (!format) { pw_stream_finish_format(pw->pwStream, 0, NULL, 0); return; } spa_format_video_raw_parse (format, &pw->videoFormat, &pw->pwType->format_video); width = pw->videoFormat.size.width; height =pw->videoFormat.size.height; stride = SPA_ROUND_UP_N (width * bpp, 4); size = height * stride; pod_builder = SPA_POD_BUILDER_INIT (paramsBuffer, sizeof (paramsBuffer)); params[0] = (spa_pod*) spa_pod_builder_object (&pod_builder, pw->pwCoreType->param.idBuffers, pw->pwCoreType->param_buffers.Buffers, ":", pw->pwCoreType->param_buffers.size, "i", size, ":", pw->pwCoreType->param_buffers.stride, "i", stride, ":", pw->pwCoreType->param_buffers.buffers, "iru", 16, PROP_RANGE (2, 16), ":", pw->pwCoreType->param_buffers.align, "i", 16); pw_stream_finish_format (pw->pwStream, 0, params, G_N_ELEMENTS (params)); } static const struct pw_remote_events pwRemoteEvents = { .version = PW_VERSION_REMOTE_EVENTS, .destroy = nullptr, .info_changed = nullptr, .sync_reply = nullptr, .state_changed = onStateChanged, }; static const struct pw_stream_events pwStreamEvents = { .version = PW_VERSION_STREAM_EVENTS, .destroy = nullptr, .state_changed = onStreamStateChanged, .format_changed = onStreamFormatChanged, .add_buffer = nullptr, .remove_buffer = nullptr, .new_buffer = nullptr, .need_buffer = nullptr, }; -ScreenCastStream::ScreenCastStream(QObject *parent) - : QObject(parent) +ScreenCastStream::ScreenCastStream(const QSize &resolution, QObject *parent) + : resolution(resolution) + , QObject(parent) { } ScreenCastStream::~ScreenCastStream() { if (pwType) { delete pwType; } if (pwStream) { pw_stream_destroy(pwStream); } if (pwRemote) { pw_remote_destroy(pwRemote); } if (pwCore) { pw_core_destroy(pwCore); } if (pwLoop) { pw_loop_leave(pwLoop); pw_loop_destroy(pwLoop); } } void ScreenCastStream::init() { pw_init(nullptr, nullptr); pwLoop = pw_loop_new(nullptr); socketNotifier.reset(new QSocketNotifier(pw_loop_get_fd(pwLoop), QSocketNotifier::Read)); connect(socketNotifier.data(), &QSocketNotifier::activated, this, &ScreenCastStream::processPipewireEvents); pwCore = pw_core_new(pwLoop, nullptr); pwCoreType = pw_core_get_type(pwCore); pwRemote = pw_remote_new(pwCore, nullptr, 0); spa_debug_set_type_map(pwCoreType->map); initializePwTypes(); pw_remote_add_listener(pwRemote, &remoteListener, &pwRemoteEvents, this); pw_remote_connect(pwRemote); } uint ScreenCastStream::nodeId() { if (pwStream) { return (uint)pw_stream_get_node_id(pwStream); } return 0; } -bool ScreenCastStream::createStream(const QSize &resolution) +bool ScreenCastStream::createStream() { if (pw_remote_get_state(pwRemote, nullptr) != PW_REMOTE_STATE_CONNECTED) { qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Cannot create pipewire stream"; return false; } uint8_t buffer[1024]; spa_pod_builder podBuilder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); const float frameRate = 25; spa_fraction maxFramerate; spa_fraction minFramerate; const spa_pod *params[1]; pwStream = pw_stream_new(pwRemote, "kwin-screen-cast", nullptr); PwFraction fraction = pipewireFractionFromDouble(frameRate); minFramerate = SPA_FRACTION(1, 1); maxFramerate = SPA_FRACTION((uint32_t)fraction.num, (uint32_t)fraction.denom); spa_rectangle minResolution = SPA_RECTANGLE(1, 1); int width = resolution.width(); int height = resolution.height(); spa_fraction paramFraction = SPA_FRACTION(0, 1); spa_rectangle paramRectangle = SPA_RECTANGLE((uint32_t)width, (uint32_t)height); params[0] = (spa_pod*)spa_pod_builder_object(&podBuilder, pwCoreType->param.idEnumFormat, pwCoreType->spa_format, "I", pwType->media_type.video, "I", pwType->media_subtype.raw, ":", pwType->format_video.format, "I", pwType->video_format.RGBx, ":", pwType->format_video.size, "Rru", &minResolution, SPA_POD_PROP_MIN_MAX(&width, &height), ":", pwType->format_video.framerate, "F", ¶mFraction, ":", pwType->format_video.max_framerate, "Fru", &maxFramerate, PROP_RANGE (&minFramerate, &maxFramerate)); pw_stream_add_listener(pwStream, &streamListener, &pwStreamEvents, this); if (pw_stream_connect(pwStream, PW_DIRECTION_OUTPUT, nullptr, PW_STREAM_FLAG_NONE, params, G_N_ELEMENTS(¶ms)) != 0) { qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Could not connect to stream"; return false; } return true; } bool ScreenCastStream::recordFrame(uint8_t *screenData) { uint32_t bufferId; struct spa_buffer *buffer; uint8_t *map = nullptr; uint8_t *data = nullptr; // TODO check timestamp like mutter does? if (!pwStream) { return false; } bufferId = pw_stream_get_empty_buffer(pwStream); if (bufferId == SPA_ID_INVALID) { qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to get empty stream buffer: " << strerror(errno); return false; } buffer = pw_stream_peek_buffer(pwStream, bufferId); if (buffer->datas[0].type == pwCoreType->data.MemFd) { map = (uint8_t *)mmap(nullptr, buffer->datas[0].maxsize + buffer->datas[0].mapoffset, PROT_READ | PROT_WRITE, MAP_SHARED, buffer->datas[0].fd, 0); if (map == MAP_FAILED) { qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to mmap pipewire stream buffer: " << strerror(errno); return false; } data = SPA_MEMBER(map, buffer->datas[0].mapoffset, uint8_t); } else if (buffer->datas[0].type == pwCoreType->data.MemPtr) { data = (uint8_t *) buffer->datas[0].data; } else { return false; } memcpy(data, screenData, BITS_PER_PIXEL * videoFormat.size.height * videoFormat.size.width * sizeof(uint8_t)); if (map) { munmap(map, buffer->datas[0].maxsize + buffer->datas[0].mapoffset); } buffer->datas[0].chunk->size = buffer->datas[0].maxsize; pw_stream_send_buffer(pwStream, bufferId); return true; } void ScreenCastStream::removeStream() { // FIXME destroying streams seems to be crashing, Mutter also doesn't remove them, maybe Pipewire does this automatically // pw_stream_destroy(pwStream); // pwStream = nullptr; pw_stream_disconnect(pwStream); } void ScreenCastStream::initializePwTypes() { // raw C-like ScreenCastStream type map auto map = pwCoreType->map; pwType = new PwType(); spa_type_media_type_map(map, &pwType->media_type); spa_type_media_subtype_map(map, &pwType->media_subtype); spa_type_format_video_map (map, &pwType->format_video); spa_type_video_format_map (map, &pwType->video_format); } void ScreenCastStream::processPipewireEvents() { int result = pw_loop_iterate(pwLoop, 0); if (result < 0) { qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to iterate over pipewire loop: " << spa_strerror(result); } } diff --git a/src/screencaststream.h b/src/screencaststream.h index 0ea4a9b..f500418 100644 --- a/src/screencaststream.h +++ b/src/screencaststream.h @@ -1,99 +1,100 @@ /* * Copyright © 2018 Red Hat, Inc * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #ifndef SCREEN_CAST_STREAM_H #define SCREEN_CAST_STREAM_H #include #include #include #include #include #include #include #include #include #include #include #include #include class PwType { public: spa_type_media_type media_type; spa_type_media_subtype media_subtype; spa_type_format_video format_video; spa_type_video_format video_format; }; class QSocketNotifier; class ScreenCastStream : public QObject { Q_OBJECT public: - explicit ScreenCastStream(QObject *parent = nullptr); + explicit ScreenCastStream(const QSize &resolution, QObject *parent = nullptr); ~ScreenCastStream(); // Public void init(); uint nodeId(); // Public because we need access from static functions - bool createStream(const QSize &resolution); + bool createStream(); bool recordFrame(uint8_t *screenData); void removeStream(); Q_SIGNALS: void streamReady(uint nodeId); void startStreaming(); void stopStreaming(); private: void initializePwTypes(); private Q_SLOTS: void processPipewireEvents(); public: pw_core *pwCore = nullptr; pw_loop *pwLoop = nullptr; pw_node *pwNode = nullptr; pw_stream *pwStream = nullptr; pw_type *pwCoreType = nullptr; pw_remote *pwRemote = nullptr; PwType *pwType = nullptr; spa_hook remoteListener; spa_hook streamListener; + QSize resolution; QScopedPointer socketNotifier; spa_video_info_raw videoFormat; }; #endif // SCREEN_CAST_STREAM_H