KPipewire

pipewiresourceitem.cpp
1/*
2 Render a PipeWire stream into a QtQuick scene as a standard Item
3 SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8#include "pipewiresourceitem.h"
9#include "glhelpers.h"
10#include "logging.h"
11#include "pipewiresourcestream.h"
12#include "pwhelpers.h"
13
14#include <QGuiApplication>
15#include <QOpenGLContext>
16#include <QOpenGLTexture>
17#include <QPainter>
18#include <QQuickWindow>
19#include <QRunnable>
20#include <QSGImageNode>
21#include <QSocketNotifier>
22#include <QThread>
23#include <qpa/qplatformnativeinterface.h>
24
25#include <EGL/eglext.h>
26#include <fcntl.h>
27#include <libdrm/drm_fourcc.h>
28#include <unistd.h>
29
30static void pwInit()
31{
32 pw_init(nullptr, nullptr);
33}
34Q_COREAPP_STARTUP_FUNCTION(pwInit);
35
36class PipeWireSourceItemPrivate
37{
38public:
39 uint m_nodeId = 0;
40 std::optional<uint> m_fd;
41 std::function<QSGTexture *()> m_createNextTexture;
42 std::unique_ptr<PipeWireSourceStream> m_stream;
43 std::unique_ptr<QOpenGLTexture> m_texture;
44
45 EGLImage m_image = nullptr;
46 bool m_needsRecreateTexture = false;
47 bool m_allowDmaBuf = true;
48 bool m_ready = false;
49
50 struct {
51 QImage texture;
52 std::optional<QPoint> position;
53 QPoint hotspot;
54 bool dirty = false;
55 } m_cursor;
56 std::optional<QRegion> m_damage;
57};
58
59class DiscardEglPixmapRunnable : public QRunnable
60{
61public:
62 DiscardEglPixmapRunnable(EGLImageKHR image, QOpenGLTexture *texture)
63 : m_image(image)
64 , m_texture(texture)
65 {
66 }
67
68 void run() override
69 {
70 if (m_image != EGL_NO_IMAGE_KHR) {
71 eglDestroyImageKHR(eglGetCurrentDisplay(), m_image);
72 }
73
74 delete m_texture;
75 }
76
77private:
78 const EGLImageKHR m_image;
79 QOpenGLTexture *m_texture;
80};
81
82PipeWireSourceItem::PipeWireSourceItem(QQuickItem *parent)
83 : QQuickItem(parent)
84 , d(new PipeWireSourceItemPrivate)
85{
86 setFlag(ItemHasContents, true);
87}
88
89PipeWireSourceItem::~PipeWireSourceItem()
90{
91 if (d->m_fd) {
92 close(*d->m_fd);
93 }
94}
95
96void PipeWireSourceItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
97{
98 switch (change) {
100 if (!isVisible()) {
101 setReady(false);
102 }
103 if (d->m_stream) {
104 d->m_stream->setActive(isVisible());
105 }
106 break;
107 case ItemSceneChange:
108 d->m_needsRecreateTexture = true;
109 releaseResources();
110 break;
111 default:
112 break;
113 }
114
115 QQuickItem::itemChange(change, data);
116}
117
118void PipeWireSourceItem::releaseResources()
119{
120 if (window() && (d->m_image || d->m_texture)) {
121 window()->scheduleRenderJob(new DiscardEglPixmapRunnable(d->m_image, d->m_texture.release()), QQuickWindow::NoStage);
122 d->m_image = EGL_NO_IMAGE_KHR;
123 }
124}
125
126void PipeWireSourceItem::setFd(uint fd)
127{
128 if (fd == d->m_fd)
129 return;
130
131 if (d->m_fd) {
132 close(*d->m_fd);
133 }
134 d->m_fd = fd;
135 refresh();
136 Q_EMIT fdChanged(fd);
137}
138
139void PipeWireSourceItem::resetFd()
140{
141 if (!d->m_fd.has_value()) {
142 return;
143 }
144
145 setReady(false);
146 close(*d->m_fd);
147 d->m_fd.reset();
148 d->m_stream.reset(nullptr);
149 d->m_createNextTexture = [] {
150 return nullptr;
151 };
152 Q_EMIT streamSizeChanged();
153}
154
155void PipeWireSourceItem::refresh()
156{
157 setReady(false);
158
159 if (!isComponentComplete()) {
160 return;
161 }
162
163 if (d->m_nodeId == 0) {
164 releaseResources();
165 d->m_stream.reset(nullptr);
166 Q_EMIT streamSizeChanged();
167
168 d->m_createNextTexture = [] {
169 return nullptr;
170 };
171 } else {
172 d->m_stream.reset(new PipeWireSourceStream(this));
173 d->m_stream->setAllowDmaBuf(d->m_allowDmaBuf);
174 Q_EMIT streamSizeChanged();
175 connect(d->m_stream.get(), &PipeWireSourceStream::streamParametersChanged, this, &PipeWireSourceItem::streamSizeChanged);
176 connect(d->m_stream.get(), &PipeWireSourceStream::streamParametersChanged, this, &PipeWireSourceItem::usingDmaBufChanged);
177
178 d->m_stream->createStream(d->m_nodeId, d->m_fd.value_or(0));
179 if (!d->m_stream->error().isEmpty()) {
180 d->m_stream.reset(nullptr);
181 d->m_nodeId = 0;
182 return;
183 }
184 d->m_stream->setActive(isVisible());
185
186 connect(d->m_stream.get(), &PipeWireSourceStream::frameReceived, this, &PipeWireSourceItem::processFrame);
187 connect(d->m_stream.get(), &PipeWireSourceStream::stateChanged, this, &PipeWireSourceItem::stateChanged);
188 }
189 Q_EMIT stateChanged();
190}
191
192void PipeWireSourceItem::setNodeId(uint nodeId)
193{
194 if (nodeId == d->m_nodeId)
195 return;
196
197 d->m_nodeId = nodeId;
198 refresh();
199 Q_EMIT nodeIdChanged(nodeId);
200}
201
202class PipeWireRenderNode : public QSGNode
203{
204public:
205 QSGImageNode *screenNode(QQuickWindow *window)
206 {
207 if (!m_screenNode) {
208 m_screenNode = window->createImageNode();
209 appendChildNode(m_screenNode);
210 }
211 return m_screenNode;
212 }
213 QSGImageNode *cursorNode(QQuickWindow *window)
214 {
215 if (!m_cursorNode) {
216 m_cursorNode = window->createImageNode();
217 appendChildNode(m_cursorNode);
218 }
219 return m_cursorNode;
220 }
221
222 QSGImageNode *damageNode(QQuickWindow *window)
223 {
224 if (!m_damageNode) {
225 m_damageNode = window->createImageNode();
226 appendChildNode(m_damageNode);
227 }
228 return m_damageNode;
229 }
230
231 void discardCursor()
232 {
233 if (m_cursorNode) {
234 removeChildNode(m_cursorNode);
235 delete m_cursorNode;
236 m_cursorNode = nullptr;
237 }
238 }
239
240 void discardDamage()
241 {
242 if (m_damageNode) {
243 removeChildNode(m_damageNode);
244 delete m_damageNode;
245 m_damageNode = nullptr;
246 }
247 }
248
249private:
250 QSGImageNode *m_screenNode = nullptr;
251 QSGImageNode *m_cursorNode = nullptr;
252 QSGImageNode *m_damageNode = nullptr;
253};
254
255QSGNode *PipeWireSourceItem::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *)
256{
257 if (Q_UNLIKELY(!d->m_createNextTexture)) {
258 return node;
259 }
260
261 auto texture = d->m_createNextTexture();
262 if (!texture) {
263 delete node;
264 return nullptr;
265 }
266
267 auto pwNode = static_cast<PipeWireRenderNode *>(node);
268 if (!pwNode) {
269 pwNode = new PipeWireRenderNode;
270 }
271
272 QSGImageNode *screenNode = pwNode->screenNode(window());
273 screenNode->setTexture(texture);
274 screenNode->setOwnsTexture(true);
275
276 const auto br = boundingRect().toRect();
277 QRect rect({0, 0}, texture->textureSize().scaled(br.size(), Qt::KeepAspectRatio));
278 rect.moveCenter(br.center());
279 screenNode->setRect(rect);
280
281 if (!d->m_cursor.position.has_value() || d->m_cursor.texture.isNull()) {
282 pwNode->discardCursor();
283 } else {
284 QSGImageNode *cursorNode = pwNode->cursorNode(window());
285 if (d->m_cursor.dirty || !cursorNode->texture()) {
286 cursorNode->setTexture(window()->createTextureFromImage(d->m_cursor.texture));
287 cursorNode->setOwnsTexture(true);
288 d->m_cursor.dirty = false;
289 }
290 const qreal scale = qreal(rect.width()) / texture->textureSize().width();
291 cursorNode->setRect(QRectF{rect.topLeft() + (d->m_cursor.position.value() * scale), d->m_cursor.texture.size() * scale});
292 Q_ASSERT(cursorNode->texture());
293 }
294
295 if (!d->m_damage || d->m_damage->isEmpty()) {
296 pwNode->discardDamage();
297 } else {
298 auto *damageNode = pwNode->damageNode(window());
299 QImage damageImage(texture->textureSize(), QImage::Format_RGBA64_Premultiplied);
300 damageImage.fill(Qt::transparent);
301 QPainter p(&damageImage);
302 p.setBrush(Qt::red);
303 for (auto rect : *d->m_damage) {
304 p.drawRect(rect);
305 }
306 damageNode->setTexture(window()->createTextureFromImage(damageImage));
307 damageNode->setOwnsTexture(true);
308 damageNode->setRect(rect);
309 Q_ASSERT(damageNode->texture());
310 }
311 return pwNode;
312}
313
314QString PipeWireSourceItem::error() const
315{
316 return d->m_stream->error();
317}
318
319void PipeWireSourceItem::processFrame(const PipeWireFrame &frame)
320{
321 d->m_damage = frame.damage;
322
323 if (frame.cursor) {
324 d->m_cursor.position = frame.cursor->position;
325 d->m_cursor.hotspot = frame.cursor->hotspot;
326 if (!frame.cursor->texture.isNull()) {
327 d->m_cursor.dirty = true;
328 d->m_cursor.texture = frame.cursor->texture;
329 }
330 } else {
331 d->m_cursor.position = std::nullopt;
332 d->m_cursor.hotspot = {};
333 }
334
335 if (frame.dmabuf) {
336 updateTextureDmaBuf(*frame.dmabuf, frame.format);
337 } else if (frame.dataFrame) {
338 updateTextureImage(frame.dataFrame);
339 }
340
341 if (window() && window()->isVisible()) {
342 update();
343 }
344}
345
346void PipeWireSourceItem::updateTextureDmaBuf(const DmaBufAttributes &attribs, spa_video_format format)
347{
348 if (!window()) {
349 qCWarning(PIPEWIRE_LOGGING) << "Window not available" << this;
350 return;
351 }
352
354 if (!openglContext || !d->m_stream) {
355 qCWarning(PIPEWIRE_LOGGING) << "need a window and a context" << window();
356 return;
357 }
358
359 d->m_createNextTexture = [this, format, attribs]() -> QSGTexture * {
360 const EGLDisplay display = static_cast<EGLDisplay>(QGuiApplication::platformNativeInterface()->nativeResourceForIntegration("egldisplay"));
361 if (d->m_image) {
362 eglDestroyImageKHR(display, d->m_image);
363 }
364 const auto size = d->m_stream->size();
365 d->m_image = GLHelpers::createImage(display, attribs, PipeWireSourceStream::spaVideoFormatToDrmFormat(format), size, nullptr);
366 if (d->m_image == EGL_NO_IMAGE_KHR) {
367 QMetaObject::invokeMethod(this, [this, format, attribs]() {
368 d->m_stream->renegotiateModifierFailed(format, attribs.modifier);
370 return nullptr;
371 }
372 if (!d->m_texture) {
373 d->m_texture.reset(new QOpenGLTexture(QOpenGLTexture::Target2D));
374 bool created = d->m_texture->create();
375 Q_ASSERT(created);
376 }
377
378 GLHelpers::initDebugOutput();
379 d->m_texture->bind();
380
381 glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)d->m_image);
382
383 d->m_texture->setWrapMode(QOpenGLTexture::ClampToEdge);
384 d->m_texture->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
385 d->m_texture->release();
386 d->m_texture->setSize(size.width(), size.height());
387
388 int textureId = d->m_texture->textureId();
390 format == SPA_VIDEO_FORMAT_ARGB || format == SPA_VIDEO_FORMAT_BGRA ? QQuickWindow::TextureHasAlphaChannel : QQuickWindow::TextureIsOpaque;
391 return QNativeInterface::QSGOpenGLTexture::fromNative(textureId, window(), size, textureOption);
392 };
393
394 setReady(true);
395}
396
397void PipeWireSourceItem::updateTextureImage(const std::shared_ptr<PipeWireFrameData> &data)
398{
399 if (!window()) {
400 qCWarning(PIPEWIRE_LOGGING) << "pass";
401 return;
402 }
403
404 d->m_createNextTexture = [this, data] {
406 };
407
408 setReady(true);
409}
410
411void PipeWireSourceItem::componentComplete()
412{
414 if (d->m_nodeId != 0) {
415 refresh();
416 }
417}
418
419PipeWireSourceItem::StreamState PipeWireSourceItem::state() const
420{
421 if (!d->m_stream) {
422 return StreamState::Unconnected;
423 }
424 switch (d->m_stream->state()) {
425 case PW_STREAM_STATE_ERROR:
426 return StreamState::Error;
427 case PW_STREAM_STATE_UNCONNECTED:
428 return StreamState::Unconnected;
429 case PW_STREAM_STATE_CONNECTING:
430 return StreamState::Connecting;
431 case PW_STREAM_STATE_PAUSED:
432 return StreamState::Paused;
433 case PW_STREAM_STATE_STREAMING:
434 return StreamState::Streaming;
435 default:
436 return StreamState::Error;
437 }
438}
439
440uint PipeWireSourceItem::fd() const
441{
442 return d->m_fd.value_or(0);
443}
444
445uint PipeWireSourceItem::nodeId() const
446{
447 return d->m_nodeId;
448}
449
450QSize PipeWireSourceItem::streamSize() const
451{
452 if (!d->m_stream) {
453 return QSize();
454 }
455 return d->m_stream->size();
456}
457
458bool PipeWireSourceItem::usingDmaBuf() const
459{
460 return d->m_stream && d->m_stream->usingDmaBuf();
461}
462
463bool PipeWireSourceItem::allowDmaBuf() const
464{
465 return d->m_stream && d->m_stream->allowDmaBuf();
466}
467
468void PipeWireSourceItem::setAllowDmaBuf(bool allowed)
469{
470 d->m_allowDmaBuf = allowed;
471 if (d->m_stream) {
472 d->m_stream->setAllowDmaBuf(allowed);
473 }
474}
475
476void PipeWireSourceItem::setReady(bool ready)
477{
478 if (d->m_ready != ready) {
479 d->m_ready = ready;
480 Q_EMIT readyChanged();
481 }
482}
483
484bool PipeWireSourceItem::isReady() const
485{
486 return d->m_ready;
487}
488
489#include "moc_pipewiresourceitem.cpp"
KGUIADDONS_EXPORT QWindow * window(QObject *job)
const QList< QKeySequence > & close()
Format_RGBA64_Premultiplied
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
QSGTexture * fromNative(GLuint textureId, QQuickWindow *window, const QSize &size, QQuickWindow::CreateTextureOptions options)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
virtual QRectF boundingRect() const const
virtual void componentComplete() override
bool isComponentComplete() const const
virtual void itemChange(ItemChange change, const ItemChangeData &value)
QSizeF size() const const
void update()
bool isVisible() const const
QQuickWindow * window() const const
QSGTexture * createTextureFromImage(const QImage &image) const const
QSGRendererInterface * rendererInterface() const const
void scheduleRenderJob(QRunnable *job, RenderStage stage)
QRect toRect() const const
virtual void setOwnsTexture(bool owns)=0
virtual void setRect(const QRectF &rect)=0
virtual void setTexture(QSGTexture *texture)=0
virtual QSGTexture * texture() const const=0
void appendChildNode(QSGNode *node)
void removeChildNode(QSGNode *node)
virtual void * getResource(QQuickWindow *window, Resource resource) const const
qreal height() const const
qreal width() const const
KeepAspectRatio
QueuedConnection
transparent
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:05:20 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.