From 9f590c5271c0b1e044e7f5c72e76c5e35c3b0f24 Mon Sep 17 00:00:00 2001 From: Yixue Wang Date: Fri, 14 Jun 2024 18:08:27 +0800 Subject: [PATCH] feat: implement interactive capture Implement interactive capture using protocol treeland-capture. Only works on treeland. --- .reuse/dep5 | 4 + src/wayland/CMakeLists.txt | 3 + src/wayland/portalwaylandcontext.cpp | 1 + src/wayland/portalwaylandcontext.h | 4 + .../treeland-capture-unstable-v1.xml | 217 ++++++++++++++++++ src/wayland/protocols/treelandcapture.cpp | 102 ++++++++ src/wayland/protocols/treelandcapture.h | 105 +++++++++ src/wayland/screenshotportal.cpp | 40 +++- src/wayland/screenshotportal.h | 1 + 9 files changed, 475 insertions(+), 2 deletions(-) create mode 100644 src/wayland/protocols/treeland-capture-unstable-v1.xml create mode 100644 src/wayland/protocols/treelandcapture.cpp create mode 100644 src/wayland/protocols/treelandcapture.h diff --git a/.reuse/dep5 b/.reuse/dep5 index 5a21c38..8ddbd36 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -47,3 +47,7 @@ License: CC0-1.0 Files: xml/*.xml Copyright: None License: CC0-1.0 + +Files: src/wayland/protocols/treeland-capture-unstable-v1.xml +Copyright: UnionTech Software Technology Co., Ltd. +License: CC0-1.0 diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 06fc165..d6ac3f7 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -11,10 +11,13 @@ add_library(xdg-desktop-portal-dde-wayland SHARED protocols/screencopy.h protocols/screencopy.cpp protocols/common.h + protocols/treelandcapture.h + protocols/treelandcapture.cpp ) qt_generate_wayland_protocol_client_sources(xdg-desktop-portal-dde-wayland FILES ${WlrProtocols_PKGDATADIR}/unstable/wlr-screencopy-unstable-v1.xml + ${CMAKE_CURRENT_LIST_DIR}/protocols/treeland-capture-unstable-v1.xml ) target_include_directories(xdg-desktop-portal-dde-wayland diff --git a/src/wayland/portalwaylandcontext.cpp b/src/wayland/portalwaylandcontext.cpp index 160ecae..37509d0 100644 --- a/src/wayland/portalwaylandcontext.cpp +++ b/src/wayland/portalwaylandcontext.cpp @@ -17,6 +17,7 @@ PortalWaylandContext::PortalWaylandContext(QObject *parent) : QObject(parent) , QDBusContext() , m_screenCopyManager(new ScreenCopyManager(this)) + , m_treelandCaptureManager(new TreeLandCaptureManager(this)) { auto screenShotPortal = new ScreenshotPortalWayland(this); } diff --git a/src/wayland/portalwaylandcontext.h b/src/wayland/portalwaylandcontext.h index 1878a19..13a8720 100644 --- a/src/wayland/portalwaylandcontext.h +++ b/src/wayland/portalwaylandcontext.h @@ -5,6 +5,7 @@ #pragma once #include "protocols/screencopy.h" +#include "protocols/treelandcapture.h" #include #include @@ -16,6 +17,9 @@ class PortalWaylandContext : public QObject, public QDBusContext public: PortalWaylandContext(QObject *parent = nullptr); inline QPointer screenCopyManager() { return m_screenCopyManager; } + inline QPointer treelandCaptureManager() { return m_treelandCaptureManager; } + private: ScreenCopyManager *m_screenCopyManager; + TreeLandCaptureManager *m_treelandCaptureManager; }; diff --git a/src/wayland/protocols/treeland-capture-unstable-v1.xml b/src/wayland/protocols/treeland-capture-unstable-v1.xml new file mode 100644 index 0000000..9f4b86f --- /dev/null +++ b/src/wayland/protocols/treeland-capture-unstable-v1.xml @@ -0,0 +1,217 @@ + + + + This protocol allows authorized application to capture output contents or window + contents(useful for window streaming). + + + + + + + + + + + + + + + + + Unreferences the frame. This request must be called as soon as it's no longer valid. + + + + + + Start session and keeps sending frame. + + + + + + Main event supplying the client with information about the frame. If the capture didn't fail, this event is always + emitted first before any other events. + When mask is provided, x and y should be offset relative to mask surface origin. Otherwise offset_x and offset_y should always + be zero. + + + + + + + + + + + + + + + + + + + + + + + + + + + This event is sent as soon as the frame is presented, indicating it is available for reading. This event + includes the time at which presentation happened at. + + + + + + + + + If the capture failed or if the frame is no longer valid after the "frame" event has been emitted, this + event will be used to inform the client to scrap the frame. + + + + + + + + + + Destroys the context. This request can be sent at any time by the client. + + + + + + + + + Inform client to prepare buffer. + + + + + + + + + + Inform client that all buffer formats supported are emitted. + + + + + + Copy capture contents to provided buffer + + + + + + + Provides flags about the frame. This event is sent once before the + "ready" event. + + + + + + + Inform that buffer is ready for reading + + + + + + Inform that frame copy fails. + + + + + + + + + Destroys the context. This request can be sent at any time by the client. + + + + + + + + + + + + + + + + + + + Selector is provided by compositor. Client can provide source hint to hint compositor + to provide certain kinds of source. + + + + + + + + + + This event supplies the client some information about the capture source, including + the capture region relative to mask and source type. + + + + + + + + + + + There could a lot of reasons but the most common one is that selector is busy + + + + + + + This event can be called just once. A second call might result in a protocol error cause + we just provide transient + + + + + + + Often used by a screen recorder. + + + + + + + + + + Destroy the treeland_capture_manager_v1 object. + + + + + + + + + + diff --git a/src/wayland/protocols/treelandcapture.cpp b/src/wayland/protocols/treelandcapture.cpp new file mode 100644 index 0000000..7949405 --- /dev/null +++ b/src/wayland/protocols/treelandcapture.cpp @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "treelandcapture.h" +#include "common.h" + +Q_DECLARE_LOGGING_CATEGORY(portalWaylandProtocol); +void destruct_treeland_capture_manager(TreeLandCaptureManager *manager) +{ + qDeleteAll(manager->captureContexts); + manager->captureContexts.clear(); +} + + +QPointer TreeLandCaptureManager::getContext() +{ + auto context = get_context(); + auto captureContext = new TreeLandCaptureContext(context); + captureContexts.append(captureContext); + return captureContext; +} + +TreeLandCaptureContext::TreeLandCaptureContext(struct ::treeland_capture_context_v1 *object) + : QObject() + , QtWayland::treeland_capture_context_v1(object) + , m_captureFrame(nullptr) +{} + +void TreeLandCaptureContext::treeland_capture_context_v1_source_ready(int32_t region_x, int32_t region_y, uint32_t region_width, uint32_t region_height, uint32_t source_type) +{ + Q_EMIT sourceReady(QRect(region_x, region_y, region_width, region_height), source_type); +} + +void TreeLandCaptureContext::treeland_capture_context_v1_source_failed(uint32_t reason) +{ + Q_EMIT sourceFailed(reason); +} + +QPointer TreeLandCaptureContext::frame() +{ + if (m_captureFrame) + return m_captureFrame; + auto capture_frame = capture(); + m_captureFrame = new TreeLandCaptureFrame(capture_frame); + return m_captureFrame; +} + +void TreeLandCaptureContext::selectSource(uint32_t sourceHint, bool freeze, bool withCursor, ::wl_surface *mask) +{ + select_source(sourceHint, freeze, withCursor, mask); +} +void TreeLandCaptureContext::releaseCaptureFrame() { + if (m_captureFrame) { + delete m_captureFrame; + m_captureFrame = nullptr; + } +} + +void TreeLandCaptureFrame::treeland_capture_frame_v1_buffer(uint32_t format, uint32_t width, uint32_t height, uint32_t stride) +{ + if (stride != width * 4) { + qCDebug(portalWaylandProtocol) + << "Receive a buffer format which is not compatible with QWaylandShmBuffer." + << "format:" << format << "width:" << width << "height:" << height + << "stride:" << stride; + return; + } + if (m_pendingShmBuffer) + return; // We only need one supported format + m_pendingShmBuffer = new QtWaylandClient::QWaylandShmBuffer(waylandDisplay(), QSize(width, height), QtWaylandClient::QWaylandShm::formatFrom(static_cast<::wl_shm_format>(format))); + copy(m_pendingShmBuffer->buffer()); +} + +void TreeLandCaptureFrame::treeland_capture_frame_v1_flags(uint32_t flags) +{ + m_flags = flags; +} + +void TreeLandCaptureFrame::treeland_capture_frame_v1_ready() +{ + if (m_shmBuffer) + delete m_shmBuffer; + m_shmBuffer = m_pendingShmBuffer; + m_pendingShmBuffer = nullptr; + Q_EMIT ready(*m_shmBuffer->image()); +} + +void TreeLandCaptureFrame::treeland_capture_frame_v1_failed() +{ + Q_EMIT failed(); +} + +void TreeLandCaptureManager::releaseCaptureContext(QPointer context) +{ + for (const auto &entry : captureContexts) { + if (entry == context.data()) { + entry->deleteLater(); + captureContexts.removeOne(entry); + } + } +} diff --git a/src/wayland/protocols/treelandcapture.h b/src/wayland/protocols/treelandcapture.h new file mode 100644 index 0000000..f205458 --- /dev/null +++ b/src/wayland/protocols/treelandcapture.h @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include "qwayland-treeland-capture-unstable-v1.h" + +#include +#include + +class TreeLandCaptureFrame : public QObject, public QtWayland::treeland_capture_frame_v1 +{ + Q_OBJECT +public: + explicit TreeLandCaptureFrame(struct ::treeland_capture_frame_v1 *object) + : QObject() + , QtWayland::treeland_capture_frame_v1(object) + , m_shmBuffer(nullptr) + , m_pendingShmBuffer(nullptr) + , m_flags(0) + { } + + ~TreeLandCaptureFrame() override + { + delete m_shmBuffer; + delete m_pendingShmBuffer; + destroy(); + } + + inline uint flags() const { return m_flags; } + +Q_SIGNALS: + void ready(QImage image); + void failed(); + +protected: + void treeland_capture_frame_v1_buffer(uint32_t format, uint32_t width, uint32_t height, uint32_t stride) override; + void treeland_capture_frame_v1_flags(uint32_t flags) override; + void treeland_capture_frame_v1_ready() override; + void treeland_capture_frame_v1_failed() override; + +private: + QtWaylandClient::QWaylandShmBuffer *m_shmBuffer; + QtWaylandClient::QWaylandShmBuffer *m_pendingShmBuffer; + uint m_flags; +}; + +class TreeLandCaptureContext : public QObject, public QtWayland::treeland_capture_context_v1 +{ + Q_OBJECT +public: + explicit TreeLandCaptureContext(struct ::treeland_capture_context_v1 *object); + ~TreeLandCaptureContext() override + { + releaseCaptureFrame(); + destroy(); + } + + inline QRect captureRegion() const { return m_captureRegion; } + inline QtWayland::treeland_capture_context_v1::source_type sourceType() const { return m_sourceType; } + + QPointer frame(); + void selectSource(uint32_t sourceHint, bool freeze, bool withCursor, ::wl_surface *mask); + void releaseCaptureFrame(); + +Q_SIGNALS: + void sourceReady(QRect region, uint32_t sourceType); + void sourceFailed(uint32_t reason); + +protected: + void treeland_capture_context_v1_source_ready(int32_t region_x, int32_t region_y, uint32_t region_width, uint32_t region_height, uint32_t source_type) override; + void treeland_capture_context_v1_source_failed(uint32_t reason) override; + +private: + QRect m_captureRegion; + TreeLandCaptureFrame *m_captureFrame; + QtWayland::treeland_capture_context_v1::source_type m_sourceType; +}; + +class TreeLandCaptureManager; +void destruct_treeland_capture_manager(TreeLandCaptureManager *manager); + +class TreeLandCaptureManager : public QWaylandClientExtensionTemplate, + public QtWayland::treeland_capture_manager_v1 +{ + Q_OBJECT +public: + explicit TreeLandCaptureManager(QObject *parent = nullptr) + : QWaylandClientExtensionTemplate(1) + , QtWayland::treeland_capture_manager_v1() + { } + + ~TreeLandCaptureManager() override + { + destroy(); + } + + QPointer getContext(); + void releaseCaptureContext(QPointer context); + +private: + QList captureContexts; + friend void destruct_treeland_capture_manager(TreeLandCaptureManager *manager); +}; diff --git a/src/wayland/screenshotportal.cpp b/src/wayland/screenshotportal.cpp index e3295f6..8e1bfc2 100644 --- a/src/wayland/screenshotportal.cpp +++ b/src/wayland/screenshotportal.cpp @@ -4,6 +4,7 @@ #include "screenshotportal.h" #include "protocols/common.h" +#include "protocols/treelandcapture.h" #include #include @@ -72,7 +73,7 @@ QString ScreenshotPortalWayland::fullScreenShot() QImage image(outputRegion.boundingRect().size(), formatLast); QPainter p(&image); p.setRenderHint(QPainter::Antialiasing); - for (auto info : captureList) { + for (const auto &info : std::as_const(captureList)) { if (!info->capturedImage.isNull()) { QRect targetRect = info->screen->geometry(); // Convert to screen image local coordinates @@ -94,6 +95,41 @@ QString ScreenshotPortalWayland::fullScreenShot() return ""; } } +QString ScreenshotPortalWayland::captureInteractively() +{ + auto captureManager = context()->treelandCaptureManager(); + auto captureContext = captureManager->getContext(); + if (!captureContext) { + return ""; + } + captureContext->selectSource(QtWayland::treeland_capture_context_v1::source_type_output + | QtWayland::treeland_capture_context_v1::source_type_window + | QtWayland::treeland_capture_context_v1::source_type_region + ,true + , false + ,nullptr); + QEventLoop loop; + connect(captureContext, &TreeLandCaptureContext::sourceReady, &loop, &QEventLoop::quit); + loop.exec(); + auto frame = captureContext->frame(); + QImage result; + connect(frame, &TreeLandCaptureFrame::ready, this, [this, &result, &loop](QImage image) { + result = image; + loop.quit(); + }); + connect(frame, &TreeLandCaptureFrame::failed, &loop, &QEventLoop::quit); + loop.exec(); + if (result.isNull()) return ""; + auto saveBasePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + QDir saveBaseDir(saveBasePath); + if (!saveBaseDir.exists()) return ""; + QString picName = "portal screenshot - " + QDateTime::currentDateTime().toString() + ".png"; + if (result.save(saveBaseDir.absoluteFilePath(picName), "PNG")) { + return saveBaseDir.absoluteFilePath(picName); + } else { + return ""; + } +} uint ScreenshotPortalWayland::Screenshot(const QDBusObjectPath &handle, const QString &app_id, @@ -106,7 +142,7 @@ uint ScreenshotPortalWayland::Screenshot(const QDBusObjectPath &handle, } QString filePath; if (options["interactive"].toBool()) { - // TODO Select area as crop geometry, might delegate to treeland + filePath = captureInteractively(); } else { filePath = fullScreenShot(); } diff --git a/src/wayland/screenshotportal.h b/src/wayland/screenshotportal.h index 617244f..d4be0a9 100644 --- a/src/wayland/screenshotportal.h +++ b/src/wayland/screenshotportal.h @@ -18,6 +18,7 @@ class ScreenshotPortalWayland : public AbstractWaylandPortal ScreenshotPortalWayland(PortalWaylandContext *context); QString fullScreenShot(); + QString captureInteractively(); public Q_SLOTS: uint PickColor(const QDBusObjectPath &handle,