diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -374,6 +374,21 @@ HAVE_SCHED_RESET_ON_FORK "Required for running kwin_wayland with real-time scheduling") + +option(ENABLE_PIPEWIRE "Disable PipeWire support. PipeWire is needed for screen sharing and remote desktop" ON) +if(ENABLE_PIPEWIRE) + set(HAVE_PIPEWIRE_SUPPORT 1) +else() + message(STATUS "Disabling PipeWire support") + set(HAVE_PIPEWIRE_SUPPORT 0) +endif() +add_definitions(-DHAVE_PIPEWIRE_SUPPORT=${HAVE_PIPEWIRE_SUPPORT}) + +if(HAVE_PIPEWIRE_SUPPORT) + pkg_check_modules(PipeWire IMPORTED_TARGET libpipewire-0.3) + add_feature_info(PipeWire PipeWire_FOUND "Required for screencast portal") +endif() + configure_file(config-kwin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kwin.h) ########### global ############### @@ -520,6 +535,12 @@ xwl/xwayland_interface.cpp ) +if(HAVE_PIPEWIRE_SUPPORT) + set(kwin_SRCS ${kwin_SRCS} + screencaststream.cpp + ) +endif() + if (CMAKE_SYSTEM_NAME MATCHES "Linux") set(kwin_SRCS ${kwin_SRCS} @@ -676,6 +697,11 @@ ) target_link_libraries(kwin ${kwinLibs}) + +if(HAVE_PIPEWIRE_SUPPORT) + target_link_libraries(kwin PkgConfig::PipeWire) +endif() + generate_export_header(kwin EXPORT_FILE_NAME kwin_export.h) target_link_libraries(kwin kwinglutils ${epoxy_LIBRARY}) diff --git a/abstract_client.h b/abstract_client.h --- a/abstract_client.h +++ b/abstract_client.h @@ -868,7 +868,7 @@ /** * Return window management interface */ - KWaylandServer::PlasmaWindowInterface *windowManagementInterface() const { + KWaylandServer::PlasmaWindowInterface *windowManagementInterface() const override { return m_windowManagementInterface; } diff --git a/libinput/connection.cpp b/libinput/connection.cpp --- a/libinput/connection.cpp +++ b/libinput/connection.cpp @@ -656,6 +656,9 @@ void Connection::applyScreenToDevice(Device *device) { #ifndef KWIN_BUILD_TESTING + if (!screens()) + return; + QMutexLocker locker(&m_mutex); if (!device->isTouch()) { return; diff --git a/plugins/platforms/drm/egl_gbm_backend.h b/plugins/platforms/drm/egl_gbm_backend.h --- a/plugins/platforms/drm/egl_gbm_backend.h +++ b/plugins/platforms/drm/egl_gbm_backend.h @@ -21,15 +21,19 @@ #define KWIN_EGL_GBM_BACKEND_H #include "abstract_egl_backend.h" #include "remoteaccess_manager.h" +#include +#include #include struct gbm_surface; +class ScreenCastStream; namespace KWin { class DrmBackend; class DrmBuffer; +class DrmSurfaceBuffer; class DrmOutput; class GbmSurface; @@ -52,6 +56,9 @@ QRegion prepareRenderingForScreen(int screenId) override; void init() override; +Q_SIGNALS: + void passBuffer(DrmOutput *output, DrmSurfaceBuffer* gbmbuf); + protected: void present() override; void cleanupSurfaces() override; @@ -61,6 +68,8 @@ bool initBufferConfigs(); bool initRenderingContext(); void initRemotePresent(); + + void streamingRequested(KWaylandServer::ScreencastStreamInterface* stream, ::wl_resource *output); struct Output { DrmOutput *output = nullptr; DrmBuffer *buffer = nullptr; @@ -101,6 +110,7 @@ DrmBackend *m_backend; QVector m_outputs; + QVector m_streams; QScopedPointer m_remoteaccessManager; friend class EglGbmTexture; }; diff --git a/plugins/platforms/drm/egl_gbm_backend.cpp b/plugins/platforms/drm/egl_gbm_backend.cpp --- a/plugins/platforms/drm/egl_gbm_backend.cpp +++ b/plugins/platforms/drm/egl_gbm_backend.cpp @@ -25,11 +25,15 @@ #include "gbm_surface.h" #include "logging.h" #include "options.h" +#include "screencaststream.h" #include "screens.h" +#include "wayland_server.h" + // kwin libs #include // Qt #include +#include // system #include @@ -45,6 +49,8 @@ setSyncsToVBlank(true); connect(m_backend, &DrmBackend::outputAdded, this, &EglGbmBackend::createOutput); connect(m_backend, &DrmBackend::outputRemoved, this, &EglGbmBackend::removeOutput); + + connect(waylandServer()->screencast(), &KWaylandServer::ScreencastInterface::outputScreencastRequested, this, &EglGbmBackend::streamingRequested); } EglGbmBackend::~EglGbmBackend() @@ -251,6 +257,7 @@ if (it == m_outputs.end()) { return; } + cleanupOutput(*it); m_outputs.erase(it); } @@ -441,12 +448,14 @@ void EglGbmBackend::presentOnOutput(Output &output) { eglSwapBuffers(eglDisplay(), output.eglSurface); - output.buffer = m_backend->createBuffer(output.gbmSurface); + auto surfaceBuffer = m_backend->createBuffer(output.gbmSurface); + output.buffer = surfaceBuffer; - if(m_remoteaccessManager && gbm_surface_has_free_buffers(output.gbmSurface->surface())) { + if (gbm_surface_has_free_buffers(output.gbmSurface->surface())) { // GBM surface is released on page flip so // we should pass the buffer before it's presented. - m_remoteaccessManager->passBuffer(output.output, output.buffer); + + Q_EMIT passBuffer(output.output, surfaceBuffer); } m_backend->present(output.buffer, output.output); @@ -565,6 +574,123 @@ return true; } +static void recordFrame(AbstractEglBackend* backend, ScreenCastStream* stream, gbm_bo *bo) +{ + struct pw_buffer *buffer; + struct spa_buffer *spa_buffer; + + auto pwStream = stream->pwStream; + if (!(buffer = pw_stream_dequeue_buffer(pwStream))) { + //qCWarning(KWIN_DRM) << "Failed to record frame: couldn't obtain PipeWire buffer"; + return; + } + + spa_buffer = buffer->buffer; + + uint8_t *data = (uint8_t *) spa_buffer->datas[0].data; + if (!data) { + qCWarning(KWIN_DRM) << "Failed to record frame: invalid buffer data"; + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + const quint32 height = gbm_bo_get_height(bo); + const quint32 stride = gbm_bo_get_stride(bo); + + const quint32 minStride = SPA_ROUND_UP_N(stream->videoFormat.size.width * stream->bytesPerPixel(), 4); + const quint32 minSrcSize = height * minStride; + + const quint32 srcSize = height * stride; + const quint32 destSize = spa_buffer->datas[0].maxsize; + + // If we can fit source into the pipewire buffer, we can use stride we got from gbm_bo as the client + // should be able to handle it + quint32 streamStride; + if (srcSize <= destSize) { + streamStride = stride; + // Fallback to fixed minimum stride, which should be (width * bpp) + } else if (minSrcSize == destSize) { + streamStride = minStride; + } else { + qCWarning(KWIN_DRM) << "Failed to record frame: got buffer with higher stride than we can handle"; + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + // bind context to render thread + eglMakeCurrent(backend->eglDisplay(), EGL_NO_SURFACE, EGL_NO_SURFACE, backend->context()); + + // create EGL image from imported BO + EGLImageKHR image = eglCreateImageKHR(backend->eglDisplay(), nullptr, EGL_NATIVE_PIXMAP_KHR, bo, nullptr); + + if (image == EGL_NO_IMAGE_KHR) { + qCWarning(KWIN_DRM) << "Failed to record frame: Error creating EGLImageKHR - " << glGetError(); + pw_stream_queue_buffer(pwStream, buffer); + 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); + + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + + glDeleteTextures(1, &texture); + eglDestroyImageKHR(backend->eglDisplay(), image); + + spa_buffer->datas[0].chunk->offset = 0; + spa_buffer->datas[0].chunk->size = spa_buffer->datas[0].maxsize; + spa_buffer->datas[0].chunk->stride = streamStride; + + pw_stream_queue_buffer(pwStream, buffer); +} + +void EglGbmBackend::streamingRequested(KWaylandServer::ScreencastStreamInterface* waylandStream, ::wl_resource *outputResource) +{ + auto output = KWaylandServer::OutputInterface::get(outputResource); + if (!output) { + waylandStream->sendFailed(i18n("Invalid output")); + return; + } + + DrmOutput* drmOutput = nullptr; + for (auto o : qAsConst(m_outputs)) { + if (o.output->waylandOutput() == output) + drmOutput = o.output; + } + if (!drmOutput) { + waylandStream->sendFailed(i18n("Could not find output")); + return; + } + + auto stream = new ScreenCastStream(output->pixelSize(), this); + if (stream->init(4)) { + connect(waylandStream, &KWaylandServer::ScreencastStreamInterface::stop, stream, &ScreenCastStream::stop); + QObject::connect(stream, &ScreenCastStream::streamReady, waylandStream, [output, waylandStream] (quint32 nodeId) { + waylandStream->sendCreated(nodeId, output->pixelSize()); + }); + connect(this, &EglGbmBackend::passBuffer, stream, [this, drmOutput, stream] (DrmOutput *output, DrmSurfaceBuffer* gbmbuf) { + if (drmOutput == output) { + auto bo = gbmbuf->getBo(); + recordFrame(this, stream, bo); + } + }); + connect(stream, &ScreenCastStream::stopStreaming, this, [waylandStream, stream] () { + waylandStream->sendClosed(); + delete stream; + }); + } else { + waylandStream->sendFailed(stream->error()); + delete stream; + } +} + /************************************************ * EglTexture ************************************************/ diff --git a/plugins/scenes/opengl/scene_opengl.h b/plugins/scenes/opengl/scene_opengl.h --- a/plugins/scenes/opengl/scene_opengl.h +++ b/plugins/scenes/opengl/scene_opengl.h @@ -145,8 +145,9 @@ class OpenGLWindowPixmap; -class OpenGLWindow final : public Scene::Window +class OpenGLWindow final : public QObject, public Scene::Window { + Q_OBJECT public: enum Leaf { ShadowLeaf, DecorationLeaf, ContentLeaf, PreviousContentLeaf }; @@ -188,6 +189,11 @@ WindowPixmap *createWindowPixmap() override; void performPaint(int mask, const QRegion ®ion, const WindowPaintData &data) override; + void startStreaming(KWaylandServer::ScreencastStreamInterface * stream) override; + +Q_SIGNALS: + void painted(); + private: QMatrix4x4 transformation(int mask, const WindowPaintData &data) const; GLTexture *getDecorationTexture() const; diff --git a/plugins/scenes/opengl/scene_opengl.cpp b/plugins/scenes/opengl/scene_opengl.cpp --- a/plugins/scenes/opengl/scene_opengl.cpp +++ b/plugins/scenes/opengl/scene_opengl.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include #include @@ -72,6 +73,8 @@ #include #include +#include "screencaststream.h" + // HACK: workaround for libepoxy < 1.3 #ifndef GL_GUILTY_CONTEXT_RESET #define GL_GUILTY_CONTEXT_RESET 0x8253 @@ -1528,8 +1531,78 @@ ShaderManager::instance()->popShader(); endRenderWindow(); + Q_EMIT painted(); +} + +static void recordFrame(OpenGLWindowPixmap* pixmap, ScreenCastStream* stream) +{ + auto pwStream = stream->pwStream; + struct pw_buffer *buffer = pw_stream_dequeue_buffer(pwStream); + + if (!buffer) { + qCDebug(KWIN_OPENGL) << "Failed to record frame: couldn't obtain PipeWire buffer"; + return; + } + + struct spa_buffer *spa_buffer = buffer->buffer; + struct spa_data *spa_data = spa_buffer->datas; + + const auto s = pixmap->texture()->size(); + + uint8_t *data = (uint8_t *) spa_data[0].data; + if (!data) { + qCWarning(KWIN_OPENGL) << "Failed to record frame: invalid buffer data"; + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + if (spa_buffer->datas[0].type != SPA_DATA_DmaBuf && !data) { + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + GLint streamStride; + glGetIntegerv(GL_PACK_ROW_LENGTH, &streamStride); + if (streamStride == 0) { + streamStride = SPA_ROUND_UP_N (s.width() * stream->bytesPerPixel(), 4); + } + if (uint(streamStride * s.height()) > spa_data->maxsize) { + qCDebug(KWIN_OPENGL) << "Failed to record frame: window is too big"; + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + pixmap->texture()->bind(); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + pixmap->texture()->unbind(); + + spa_data[0].chunk->offset = 0; + spa_data[0].chunk->size = spa_data[0].maxsize; + spa_data[0].chunk->stride = streamStride; + + pw_stream_queue_buffer(pwStream, buffer); } +void OpenGLWindow::startStreaming(KWaylandServer::ScreencastStreamInterface * waylandStream) +{ + auto stream = new ScreenCastStream(size(), m_scene); + if (stream->init(4)) { + QObject::connect(waylandStream, &KWaylandServer::ScreencastStreamInterface::stop, stream, &ScreenCastStream::stop); + QObject::connect(stream, &ScreenCastStream::streamReady, waylandStream, [this, waylandStream] (quint32 nodeId) { + waylandStream->sendCreated(nodeId, size()); + }); + connect(this, &OpenGLWindow::painted, stream, [this, stream] () { + recordFrame(windowPixmap(), stream); + }); + QObject::connect(stream, &ScreenCastStream::stopStreaming, waylandStream, [waylandStream, stream] () { + waylandStream->sendClosed(); + delete stream; + }); + } else { + waylandStream->sendFailed(stream->error()); + delete stream; + } +} //**************************************** // OpenGLWindowPixmap diff --git a/scene.h b/scene.h --- a/scene.h +++ b/scene.h @@ -34,6 +34,7 @@ { class BufferInterface; class SubSurfaceInterface; +class ScreencastStreamInterface; } namespace KWin @@ -347,6 +348,8 @@ void unreferencePreviousPixmap(); void invalidateQuadsCache(); void preprocess(); + virtual void startStreaming(KWaylandServer::ScreencastStreamInterface* stream); + protected: WindowQuadList makeDecorationQuads(const QRect *rects, const QRegion ®ion, qreal textureScale = 1.0) const; WindowQuadList makeContentsQuads() const; diff --git a/scene.cpp b/scene.cpp --- a/scene.cpp +++ b/scene.cpp @@ -85,6 +85,9 @@ #include #include #include +#include +#include +#include namespace KWin { @@ -97,6 +100,25 @@ : QObject(parent) { last_time.invalidate(); // Initialize the timer + + if (waylandServer()) { + connect(waylandServer()->screencast(), &KWaylandServer::ScreencastInterface::windowScreencastRequested, this, [this] (KWaylandServer::ScreencastStreamInterface* stream, quint32 winid) { + Scene::Window *window = nullptr; + for (auto it = m_windows.constBegin(), itEnd = m_windows.constEnd(); it != itEnd; ++it) { + if (it.key()->windowManagementInterface() && it.key()->windowManagementInterface()->internalId() == winid) { + window = it.value(); + break; + } + } + + if (!window) { + stream->sendFailed(i18n("Could not find window id %1", winid)); + return; + } + + window->startStreaming(stream); + }); + } } Scene::~Scene() @@ -457,9 +479,9 @@ void Scene::windowGeometryShapeChanged(Toplevel *c) { - if (!m_windows.contains(c)) // this is ok, shape is not valid by default + Window *w = m_windows.value(c); + if (!w) // this is ok, shape is not valid by default return; - Window *w = m_windows[ c ]; w->discardShape(); } @@ -1090,6 +1112,11 @@ } } +void Scene::Window::startStreaming(KWaylandServer::ScreencastStreamInterface *stream) +{ + stream->sendFailed(i18n("Streaming not supported")); +} + //**************************************** // WindowPixmap //**************************************** diff --git a/screencaststream.h b/screencaststream.h new file mode 100644 --- /dev/null +++ b/screencaststream.h @@ -0,0 +1,89 @@ +/* + * Copyright © 2018-2020 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 + */ + +#pragma once + +#include +#include + +#include "kwinglobals.h" +#include "config-kwin.h" +#include +#include +#include +#include +#include + +#undef Status + +class KWIN_EXPORT ScreenCastStream : public QObject +{ + Q_OBJECT +public: + explicit ScreenCastStream(const QSize &resolution, QObject *parent); + ~ScreenCastStream(); + + static void onCoreError(void *data, uint32_t id, int seq, int res, const char *message); + static void onStreamParamChanged(void *data, uint32_t id, const struct spa_pod *format); + static void onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message); + + bool init(int bitsPerPixel); + uint framerate(); + uint nodeId(); + QString error() const { + return m_error; + } + + void removeStream(); + void stop(); + + uint bytesPerPixel() const { + return m_bytesPerPixel; + } + +Q_SIGNALS: + void streamReady(quint32 nodeId); + void startStreaming(); + void stopStreaming(uint nodeId); + +private: + pw_stream *createStream(); +public: + struct pw_context *pwContext = nullptr; + struct pw_core *pwCore = nullptr; + struct pw_stream *pwStream = nullptr; + struct pw_thread_loop *pwMainLoop = nullptr; + + spa_hook coreListener; + spa_hook streamListener; + + // event handlers + pw_core_events pwCoreEvents = {}; + pw_stream_events pwStreamEvents = {}; + + uint32_t pwNodeId = 0; + + QSize m_resolution; + bool m_stopped = false; + + spa_video_info_raw videoFormat; + QString m_error; + uint m_bytesPerPixel = 0; +}; diff --git a/screencaststream.cpp b/screencaststream.cpp new file mode 100644 --- /dev/null +++ b/screencaststream.cpp @@ -0,0 +1,365 @@ +/* + * Copyright © 2018-2020 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 "utils.h" + +#include +#include + +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) + +static int greatestCommonDivisor(int a, int b) +{ + while (b != 0) { + int temp = a; + + a = b; + b = temp % b; + } + + return qAbs(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; + bool 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 = (int) 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 > INT_MAX || D2 > INT_MAX) + 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 - ((double) N) / D) < MAX_ERROR) + break; + + /* Take reciprocal */ + F = 1 / F; + } + + /* fix for overflow */ + if (D == 0) { + N = INT_MAX; + 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; +} + +void ScreenCastStream::stop() +{ + m_stopped = true; + delete this; +} + +void ScreenCastStream::onCoreError(void *data, uint32_t id, int seq, int res, const char *message) +{ + Q_UNUSED(seq) + ScreenCastStream *pw = static_cast(data); + + qCWarning(KWIN_CORE) << "PipeWire remote error: " << message; + + if (id == PW_ID_CORE) { + if (res == -EPIPE) { + Q_EMIT pw->stopStreaming(pw->nodeId()); + } + } +} + +void ScreenCastStream::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(KWIN_CORE) << "Stream error: " << error_message; + break; + case PW_STREAM_STATE_PAUSED: + qCDebug(KWIN_CORE) << "Stream state: " << pw_stream_state_as_string(state); + if (pw->nodeId() == 0 && pw->pwStream) { + pw->pwNodeId = pw_stream_get_node_id(pw->pwStream); + Q_EMIT pw->streamReady(pw->nodeId()); + } + if (pw->m_stopped) { + Q_EMIT pw->stopStreaming(pw->nodeId()); + } + break; + case PW_STREAM_STATE_STREAMING: + qCDebug(KWIN_CORE) << "Stream state: " << pw_stream_state_as_string(state); + Q_EMIT pw->startStreaming(); + break; + case PW_STREAM_STATE_UNCONNECTED: + case PW_STREAM_STATE_CONNECTING: + qCDebug(KWIN_CORE) << "Stream state: " << pw_stream_state_as_string(state); + break; + } +} + +void ScreenCastStream::onStreamParamChanged(void *data, uint32_t id, const struct spa_pod *format) +{ + qCDebug(KWIN_CORE) << "Stream format changed"; + + ScreenCastStream *pw = static_cast(data); + + uint8_t paramsBuffer[1024]; + int32_t width, height, stride, size; + struct spa_pod_builder pod_builder; + const struct spa_pod *params[1]; + + if (!format || id != SPA_PARAM_Format) { + return; + } + + spa_format_video_raw_parse (format, &pw->videoFormat); + + width = pw->videoFormat.size.width; + height =pw->videoFormat.size.height; + stride = SPA_ROUND_UP_N (width * pw->m_bytesPerPixel, 4); + size = height * stride; + + pod_builder = SPA_POD_BUILDER_INIT (paramsBuffer, sizeof (paramsBuffer)); + + params[0] = (spa_pod*) spa_pod_builder_add_object(&pod_builder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(16, 2, 16), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int (1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(size), + SPA_PARAM_BUFFERS_stride, SPA_POD_CHOICE_RANGE_Int(stride, stride, INT32_MAX), + SPA_PARAM_BUFFERS_align, SPA_POD_Int(16)); + pw_stream_update_params(pw->pwStream, params, 1); +} + +ScreenCastStream::ScreenCastStream(const QSize &resolution, QObject *parent) + : QObject(parent) + , m_resolution(resolution) +{ + pwCoreEvents.version = PW_VERSION_CORE_EVENTS; + pwCoreEvents.error = &onCoreError; + + pwStreamEvents.version = PW_VERSION_STREAM_EVENTS; + pwStreamEvents.state_changed = &onStreamStateChanged; + pwStreamEvents.param_changed = &onStreamParamChanged; +} + +ScreenCastStream::~ScreenCastStream() +{ + if (pwMainLoop) { + pw_thread_loop_stop(pwMainLoop); + } + + if (pwStream) { + pw_stream_destroy(pwStream); + } + + + if (pwCore) { + pw_core_disconnect(pwCore); + } + + if (pwContext) { + pw_context_destroy(pwContext); + } + + if (pwMainLoop) { + pw_thread_loop_destroy(pwMainLoop); + } +} + +bool ScreenCastStream::init(int bitsPerPixel) +{ + pw_init(nullptr, nullptr); + m_bytesPerPixel = bitsPerPixel; + + pwMainLoop = pw_thread_loop_new("pipewire-main-loop", nullptr); + pwContext = pw_context_new(pw_thread_loop_get_loop(pwMainLoop), nullptr, 0); + if (!pwContext) { + qCWarning(KWIN_CORE) << "Failed to create PipeWire context"; + m_error = i18n("Failed to create PipeWire context"); + return false; + } + + pwCore = pw_context_connect(pwContext, nullptr, 0); + if (!pwCore) { + qCWarning(KWIN_CORE) << "Failed to connect PipeWire context"; + m_error = i18n("Failed to connect PipeWire context"); + return false; + } + + pw_core_add_listener(pwCore, &coreListener, &pwCoreEvents, this); + + pwStream = createStream(); + if (!pwStream) { + qCWarning(KWIN_CORE) << "Failed to create PipeWire stream"; + m_error = i18n("Failed to create PipeWire stream"); + return false; + } + + + if (pw_thread_loop_start(pwMainLoop) < 0) { + qCWarning(KWIN_CORE) << "Failed to start main PipeWire loop"; + m_error = i18n("Failed to start main PipeWire loop"); + return false; + } + return true; +} + +uint ScreenCastStream::framerate() +{ + if (pwStream) { + return videoFormat.max_framerate.num / videoFormat.max_framerate.denom; + } + + return 0; +} + +uint ScreenCastStream::nodeId() +{ + return pwNodeId; +} + +pw_stream *ScreenCastStream::createStream() +{ + 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]; + + auto stream = pw_stream_new(pwCore, "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); + spa_rectangle maxResolution = SPA_RECTANGLE(uint32_t(m_resolution.width()), uint32_t(m_resolution.height())); + + spa_fraction paramFraction = SPA_FRACTION(0, 1); + + params[0] = (spa_pod*)spa_pod_builder_add_object(&podBuilder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_RGBx), + SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(&maxResolution, &minResolution, &maxResolution), + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(¶mFraction), + SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&maxFramerate, &minFramerate, &maxFramerate)); + + pw_stream_add_listener(stream, &streamListener, &pwStreamEvents, this); + + auto flags = static_cast(PW_STREAM_FLAG_DRIVER | PW_STREAM_FLAG_MAP_BUFFERS); + + if (pw_stream_connect(stream, PW_DIRECTION_OUTPUT, SPA_ID_INVALID, flags, params, 1) != 0) { + qCWarning(KWIN_CORE) << "Could not connect to stream"; + return nullptr; + } + + return stream; +} + +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); +} diff --git a/toplevel.h b/toplevel.h --- a/toplevel.h +++ b/toplevel.h @@ -43,6 +43,7 @@ namespace KWaylandServer { class SurfaceInterface; +class PlasmaWindowInterface; } namespace KWin @@ -343,6 +344,7 @@ * Default implementation returns same as geometry. */ virtual QRect inputGeometry() const; + virtual KWaylandServer::PlasmaWindowInterface *windowManagementInterface() const { return nullptr; } QSize size() const; QPoint pos() const; QRect rect() const; diff --git a/wayland_server.h b/wayland_server.h --- a/wayland_server.h +++ b/wayland_server.h @@ -70,6 +70,7 @@ class LinuxDmabufUnstableV1Interface; class LinuxDmabufUnstableV1Buffer; class TabletManagerInterface; +class ScreencastInterface; } @@ -119,6 +120,9 @@ KWaylandServer::PlasmaWindowManagementInterface *windowManagement() { return m_windowManagement; } + KWaylandServer::ScreencastInterface *screencast() { + return m_screencast; + } KWaylandServer::ServerSideDecorationManagerInterface *decorationManager() const { return m_decorationManager; } @@ -267,6 +271,7 @@ KWaylandServer::XdgDecorationManagerInterface *m_xdgDecorationManager = nullptr; KWaylandServer::LinuxDmabufUnstableV1Interface *m_linuxDmabuf = nullptr; QSet m_linuxDmabufBuffers; + KWaylandServer::ScreencastInterface *m_screencast = nullptr; struct { KWaylandServer::ClientConnection *client = nullptr; QMetaObject::Connection destroyConnection; diff --git a/wayland_server.cpp b/wayland_server.cpp --- a/wayland_server.cpp +++ b/wayland_server.cpp @@ -234,7 +234,7 @@ return interfaces; } - const QSet interfacesBlackList = {"org_kde_kwin_remote_access_manager", "org_kde_plasma_window_management", "org_kde_kwin_fake_input", "org_kde_kwin_keystate"}; + const QSet interfacesBlackList = {"org_kde_kwin_remote_access_manager", "org_kde_plasma_window_management", "org_kde_kwin_fake_input", "org_kde_kwin_keystate", "zkde_screencast_unstable_v1"}; QSet m_reported; bool allowInterface(KWaylandServer::ClientConnection *client, const QByteArray &interfaceName) override { @@ -453,6 +453,8 @@ m_keyState = m_display->createKeyStateInterface(m_display); m_keyState->create(); + m_screencast = m_display->createScreencastInterface(m_display); + return true; }