diff --git a/effects/CMakeLists.txt b/effects/CMakeLists.txt --- a/effects/CMakeLists.txt +++ b/effects/CMakeLists.txt @@ -20,6 +20,7 @@ KF5::Plasma # screenedge effect KF5::IconThemes KF5::Service + KF5::WaylandClient #screencast effect KF5::Notifications # screenshot effect ) @@ -176,6 +177,7 @@ add_subdirectory( mouseclick ) add_subdirectory( mousemark ) include( screenshot/CMakeLists.txt ) +include( screencast/CMakeLists.txt ) include( sheet/CMakeLists.txt ) include( snaphelper/CMakeLists.txt ) include( startupfeedback/CMakeLists.txt ) diff --git a/effects/effect_builtins.cpp b/effects/effect_builtins.cpp --- a/effects/effect_builtins.cpp +++ b/effects/effect_builtins.cpp @@ -27,6 +27,7 @@ #include "presentwindows/presentwindows.h" #include "screenedge/screenedgeeffect.h" #include "screenshot/screenshot.h" +#include "screencast/screencast.h" #include "slidingpopups/slidingpopups.h" // Common effects only relevant to desktop #include "desktopgrid/desktopgrid.h" @@ -472,6 +473,21 @@ nullptr #endif EFFECT_FALLBACK + }, { + QStringLiteral("screencast"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Screencast"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Helper effect for Screen Recorders"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + true, + true, +#ifdef EFFECT_BUILTINS + &createHelper, + &ScreenCastEffect::supported, + nullptr +#endif +EFFECT_FALLBACK }, { QStringLiteral("sheet"), i18ndc("kwin_effects", "Name of a KWin Effect", "Sheet"), diff --git a/effects/screencast/CMakeLists.txt b/effects/screencast/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/effects/screencast/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set( kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + screencast/screencast.cpp + ) diff --git a/effects/screencast/screencast.h b/effects/screencast/screencast.h new file mode 100644 --- /dev/null +++ b/effects/screencast/screencast.h @@ -0,0 +1,69 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2010 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ + +#ifndef KWIN_SCREENCAST_H +#define KWIN_SCREENCAST_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace KWin +{ + +/* + * Screencast Effect works as follows: + * - Clients call DBus method to start process + * - They pass a socket to their own wayland server + * - This server needs to export a compositor interface and an SHMPool + * - Kwin then connects as a client sending screens as a Surface + */ + +class ScreenCastEffect : public Effect, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Screencast") +public: + ScreenCastEffect(); + virtual ~ScreenCastEffect(); + virtual void postPaintScreen(); + virtual bool isActive() const; + + static bool supported(); +public Q_SLOTS: + Q_SCRIPTABLE void screencast(const QString &socket); //TODO QDBusUnixFileDescriptor + +private: + bool m_active = false; + QDBusMessage m_message; + QPointer m_surface; + QPointer m_pool; + +}; + +} // namespace + +#endif // KWIN_SCREENSHOT_H diff --git a/effects/screencast/screencast.cpp b/effects/screencast/screencast.cpp new file mode 100644 --- /dev/null +++ b/effects/screencast/screencast.cpp @@ -0,0 +1,133 @@ +#include "screencast.h" +#include +#include +#include + +#include +#include +#include + +#include +#include + +const static QString s_errorAlreadyTaking = QStringLiteral("org.kde.kwin.Screenshot.Error.AlreadyTaking"); +const static QString s_errorAlreadyTakingMsg = QStringLiteral("A screencast is already been taken"); +const static QString s_errorFailed = QStringLiteral("org.kde.kwin.Screencast.Error.Failed"); +const static QString s_errorFailedMsg = QStringLiteral("Could not connect to recording server"); + + +using namespace KWayland::Client; +using namespace KWin; + +ScreenCastEffect::ScreenCastEffect() +{ + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Screencast"), this, QDBusConnection::ExportScriptableContents); +} + +ScreenCastEffect::~ScreenCastEffect() +{ +} + +bool ScreenCastEffect::isActive() const +{ + return m_active; +} + +void ScreenCastEffect::screencast(const QString &socket) +{ + if (m_active == true) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + return; + } + m_active = true; + setDelayedReply(true); + m_message = message(); + + //designed so deleteing this object should remove all wayland client related objects + auto connectionThreadThread = new QThread(this); + + auto connectionThread = new ConnectionThread(0); + connect(connectionThreadThread, &QObject::destroyed, connectionThread, &QObject::deleteLater); + connectionThread->moveToThread(connectionThreadThread); + + connect(connectionThreadThread, &QThread::started, connectionThread, [=]() { + qDebug() << "trying to connect"; + connectionThread->setSocketName(socket); + connectionThread->initConnection(); + }); + + connect(connectionThread, &ConnectionThread::connected, this, [=]() { + auto registry = new Registry(connectionThreadThread); + registry->create(connectionThread); + + //return a non-error to show we've connected + QDBusConnection::sessionBus().send(m_message.createReply()); + + connect(registry, &Registry::compositorAnnounced, this, [registry, this](qint32 name, qint32 version) { + auto compositor = registry->createCompositor(name, version, registry); + m_surface = compositor->createSurface(); + }); + + connect(registry, &Registry::shmAnnounced, this, [registry, this](qint32 name, qint32 version) { + m_pool = registry->createShmPool(name, version, registry); + }); + + registry->setup(); + }); + + connect(connectionThread, &ConnectionThread::connectionDied, this, [=]() { + m_active = false; + delete connectionThreadThread; + }); + + connect(connectionThread, &ConnectionThread::failed, this, [=]() { + m_active = false; + delete connectionThreadThread; + sendErrorReply(s_errorFailed, s_errorFailedMsg); + }); + + connectionThreadThread->start(); +} + +void ScreenCastEffect::postPaintScreen() +{ + if (!m_surface || !m_pool) { + //nothing active. skip + return; + } + + qDebug() << "committing new image"; + + //FIXME this assumes one screen + //TODO buffer everything up like screenshot into a single buffer + //or we could return one surface per screen..but we'd need a custom interface to communicate positions? + + QRect geometry = effects->virtualScreenGeometry(); + + GLTexture tex(GL_RGBA8, geometry.width(), geometry.height()); + GLRenderTarget target(tex); + target.blitFromFramebuffer(geometry); + + // copy content from framebuffer into image + tex.bind(); + + auto img = QImage(geometry.size(), QImage::Format_RGB888); + if (GLPlatform::instance()->isGLES()) { + glReadPixels(0, 0, img.width(), img.height(), GL_RGB, GL_UNSIGNED_BYTE, (GLvoid*)img.bits()); + } else { + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, (GLvoid*)img.bits()); + } + img = img.convertToFormat(QImage::Format_ARGB32); + img = img.mirrored(); + tex.unbind(); + + auto buffer = m_pool->createBuffer(img); + m_surface->attachBuffer(buffer); + m_surface->damage(geometry); //TODO: in theory we know what area of the screen is damaged, but I can't access it, unless I buffer all regions called in the paintEvent + m_surface->commit(Surface::CommitFlag::None); +} + +bool ScreenCastEffect::supported() +{ + return effects->isOpenGLCompositing() && GLRenderTarget::supported(); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,3 +40,7 @@ add_executable(pointergestures pointergesturestest.cpp) add_definitions(-DDIR="${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(pointergestures Qt5::Gui Qt5::Quick KF5::WaylandClient) + +add_executable(screenrecorder screenrecorder.cpp) +target_link_libraries(screenrecorder Qt5::Gui Qt5::DBus KF5::WaylandServer) + diff --git a/tests/screenrecorder.cpp b/tests/screenrecorder.cpp new file mode 100644 --- /dev/null +++ b/tests/screenrecorder.cpp @@ -0,0 +1,85 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace KWayland::Server; + +/* + * This is a simple screen recorder app + * It connects to a wayland server then dumps all frames in the CWD + */ + +class Recorder : public QObject +{ + Q_OBJECT +public: + Recorder(); +private slots: + void newImage(const QImage &image); +private: + quint64 m_frame = 0; +}; + +int main(int argc, char ** argv) +{ + QGuiApplication app(argc, argv); + Recorder server; + app.exec(); +} + + +Recorder::Recorder() +{ + auto display = new Display(this); + + const QString socketName("recorder"); + + //TODO export wl_display_connect_to_fd (int fd) in Server::Display + //then pass a proper FD over DBUS + display->setSocketName(socketName); + display->start(); + display->createShm(); + + auto compositor = display->createCompositor(this); + compositor->create(); + + connect(compositor, &CompositorInterface::surfaceCreated, this, [this](SurfaceInterface* si) { + qDebug() << "kwin surface found"; + connect(si, &SurfaceInterface::damaged, this, [this, si]() { + if (!si->buffer()) return; + + auto image = si->buffer()->data(); + newImage(image); + }); + }); + + auto msg = QDBusMessage::createMethodCall("org.kde.KWin", "/Screencast", "org.kde.kwin.Screencast", "screencast"); + msg << socketName; + + auto reply = QDBusConnection::sessionBus().asyncCall(msg); + auto watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [reply, watcher]() { + watcher->deleteLater(); + if (reply.isError()) { + qWarning() << reply.error().message(); + } + }); +} + +void Recorder::newImage(const QImage &image) +{ + const QString path = "out_" + QString::number(m_frame); + image.save(path + ".png"); + m_frame++; +} + + +#include "screenrecorder.moc"