• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 30754433360

02 Aug 2026 03:28PM UTC coverage: 15.264% (+0.001%) from 15.263%
30754433360

push

github

jcelerier
fix(widgets): scale custom cursor hotspots on xcb for HiDPI

score::get_cursor loads the @2x cursor asset once devicePixelRatio() >= 1.5 and
tags the pixmap with devicePixelRatio 2. The hotspots in score::Skin are given in
device-independent pixels, which is what QCursor expects.

Qt's xcb backend however passes QCursor::hotSpot() straight to
xcb_render_create_cursor together with the raw device-pixel image
(QXcbCursor::createBitmapCursor -> qt_xcb_createCursorXRender), so the hotspot of a
64x64 @2x cursor lands at (16,16) instead of its centre (32,32). The cursor is then
drawn 16 device pixels down and to the right of the real pointer, which makes every
item look like its hitbox is misplaced: pointing at a small widget actually hits
whatever is up and to the left of it. It is most visible with the UI Zoom setting
above 100%, since Zoom raises devicePixelRatio and therefore the size of the gap in
screen pixels.

Measured with XFixesGetCursorImage on Qt 6.4.2, cursor_pointing_hand@2x.png:
hotspot (16,16) on a 64x64 image before, (32,32) after. That xcb code is unchanged
from Qt 6.2 through 6.11, so express the hotspot in the image's own pixels there
and leave the other platforms, which honour the pixmap devicePixelRatio, alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TTUi76cnUWh7dGZC64LqbZ

4 of 4 new or added lines in 1 file covered. (100.0%)

263 existing lines in 1 file now uncovered.

30463 of 199568 relevant lines covered (15.26%)

1075.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/plugins/score-plugin-gfx/Gfx/WindowCapture/WindowCapture_pipewire.cpp
1
#include <Gfx/WindowCapture/WindowCaptureBackend.hpp>
2

3
#include <ossia/detail/dylib_loader.hpp>
4

5
#include <QDebug>
6

7
#include <atomic>
8
#include <chrono>
9
#include <cstdlib>
10
#include <cstring>
11
#include <mutex>
12
#include <string>
13
#include <unistd.h>
14

15
// ─── PipeWire types and constants (no PipeWire headers needed) ───────────────
16

17
extern "C"
18
{
19
// Opaque PipeWire types
20
struct pw_thread_loop;
21
struct pw_context;
22
struct pw_core;
23
struct pw_stream;
24
struct pw_properties;
25
struct pw_loop;
26

27
// spa_hook: doubly-linked listener node.
28
// Must be fully defined because PipeWire writes into it.
29
struct spa_list
30
{
31
  struct spa_list* next;
32
  struct spa_list* prev;
33
};
34

35
struct spa_hook
36
{
37
  struct spa_list link;
38
  struct
39
  {
40
    struct spa_list link;
41
    const void* funcs;
42
    void* data;
43
  } cb;
44
  void (*removed)(struct spa_hook* hook);
45
  void* priv;
46
};
47

48
// PipeWire stream states
49
enum pw_stream_state
50
{
51
  PW_STREAM_STATE_ERROR = -1,
52
  PW_STREAM_STATE_UNCONNECTED = 0,
53
  PW_STREAM_STATE_CONNECTING = 1,
54
  PW_STREAM_STATE_PAUSED = 2,
55
  PW_STREAM_STATE_STREAMING = 3
56
};
57

58
enum pw_direction
59
{
60
  PW_DIRECTION_INPUT = 0,
61
  PW_DIRECTION_OUTPUT = 1
62
};
63

64
enum pw_stream_flags
65
{
66
  PW_STREAM_FLAG_AUTOCONNECT = (1 << 0),
67
  PW_STREAM_FLAG_MAP_BUFFERS = (1 << 2)
68
};
69

70
// SPA data types
71
enum spa_data_type
72
{
73
  SPA_DATA_Invalid = 0,
74
  SPA_DATA_MemPtr = 1,
75
  SPA_DATA_MemFd = 2,
76
  SPA_DATA_DmaBuf = 3
77
};
78

79
// SPA video formats matching PipeWire/SPA definitions
80
enum spa_video_format
81
{
82
  SPA_VIDEO_FORMAT_UNKNOWN = 0,
83
  SPA_VIDEO_FORMAT_ENCODED = 1,
84
  SPA_VIDEO_FORMAT_I420 = 2,
85
  SPA_VIDEO_FORMAT_YV12 = 3,
86
  SPA_VIDEO_FORMAT_YUY2 = 4,
87
  SPA_VIDEO_FORMAT_UYVY = 5,
88
  SPA_VIDEO_FORMAT_AYUV = 6,
89
  SPA_VIDEO_FORMAT_RGBx = 7,
90
  SPA_VIDEO_FORMAT_BGRx = 8,
91
  SPA_VIDEO_FORMAT_xRGB = 9,
92
  SPA_VIDEO_FORMAT_xBGR = 10,
93
  SPA_VIDEO_FORMAT_RGBA = 11,
94
  SPA_VIDEO_FORMAT_BGRA = 12,
95
  SPA_VIDEO_FORMAT_ARGB = 13,
96
  SPA_VIDEO_FORMAT_ABGR = 14,
97
  SPA_VIDEO_FORMAT_RGB = 15,
98
  SPA_VIDEO_FORMAT_BGR = 16
99
};
100

101
// SPA buffer layout
102
struct spa_chunk
103
{
104
  uint32_t offset;
105
  uint32_t size;
106
  int32_t stride;
107
  int32_t flags;
108
};
109

110
struct spa_data
111
{
112
  uint32_t type;
113
  uint32_t flags;
114
  int fd;
115
  uint32_t mapoffset;
116
  uint32_t maxsize;
117
  void* data;
118
  struct spa_chunk* chunk;
119
};
120

121
struct spa_meta
122
{
123
  uint32_t type;
124
  uint32_t size;
125
  void* data;
126
};
127

128
struct spa_buffer
129
{
130
  uint32_t n_metas;
131
  uint32_t n_datas;
132
  struct spa_meta* metas;
133
  struct spa_data* datas;
134
};
135

136
struct pw_buffer
137
{
138
  struct spa_buffer* buffer;
139
  void* user_data;
140
  uint64_t size;
141
  uint64_t requested;
142
};
143

144
// Stream events callback table
145
struct pw_stream_events
146
{
147
  uint32_t version;
148
  void (*destroy)(void*);
149
  void (*state_changed)(
150
      void*, enum pw_stream_state old, enum pw_stream_state state,
151
      const char* error);
152
  void (*control_info)(void*, uint32_t id, const void* info);
153
  void (*io_changed)(void*, uint32_t id, void* area, uint32_t size);
154
  void (*param_changed)(void*, uint32_t id, const void* param);
155
  void (*add_buffer)(void*, struct pw_buffer*);
156
  void (*remove_buffer)(void*, struct pw_buffer*);
157
  void (*process)(void*);
158
  void (*drained)(void*);
159
  void (*command)(void*, const void*);
160
  void (*trigger_done)(void*);
161
};
162

163
#define PW_VERSION_STREAM_EVENTS 2
164

165
// SPA pod type system identifiers
166
#define SPA_TYPE_None 1
167
#define SPA_TYPE_Bool 2
168
#define SPA_TYPE_Id 3
169
#define SPA_TYPE_Int 4
170
#define SPA_TYPE_Long 5
171
#define SPA_TYPE_Float 6
172
#define SPA_TYPE_Double 7
173
#define SPA_TYPE_String 8
174
#define SPA_TYPE_Bytes 9
175
#define SPA_TYPE_Rectangle 10
176
#define SPA_TYPE_Fraction 11
177
#define SPA_TYPE_Bitmap 12
178
#define SPA_TYPE_Array 13
179
#define SPA_TYPE_POINTER_BASE 0x10000
180
#define SPA_TYPE_Fd (SPA_TYPE_POINTER_BASE + 1)
181
#define SPA_TYPE_OBJECT_BASE 0x20000
182
#define SPA_TYPE_OBJECT_PropInfo (SPA_TYPE_OBJECT_BASE + 1)
183
#define SPA_TYPE_OBJECT_Props (SPA_TYPE_OBJECT_BASE + 2)
184
#define SPA_TYPE_OBJECT_Format (SPA_TYPE_OBJECT_BASE + 3)
185
#define SPA_TYPE_OBJECT_ParamBuffers (SPA_TYPE_OBJECT_BASE + 4)
186
#define SPA_TYPE_OBJECT_ParamMeta (SPA_TYPE_OBJECT_BASE + 5)
187
#define SPA_TYPE_OBJECT_ParamIO (SPA_TYPE_OBJECT_BASE + 6)
188

189
// SPA pod structures
190
struct spa_pod
191
{
192
  uint32_t size;
193
  uint32_t type;
194
};
195

196
struct spa_pod_object_body
197
{
198
  uint32_t type;
199
  uint32_t id;
200
};
201

202
struct spa_pod_object
203
{
204
  struct spa_pod pod;
205
  struct spa_pod_object_body body;
206
};
207

208
// SPA param IDs
209
#define SPA_PARAM_EnumFormat 3
210
#define SPA_PARAM_Format 4
211

212
// SPA format property keys
213
#define SPA_FORMAT_mediaType 1
214
#define SPA_FORMAT_mediaSubtype 2
215
#define SPA_FORMAT_VIDEO_format 3
216
#define SPA_FORMAT_VIDEO_size 4
217
#define SPA_FORMAT_VIDEO_framerate 5
218

219
// SPA media type/subtype
220
#define SPA_MEDIA_TYPE_video 2
221
#define SPA_MEDIA_SUBTYPE_raw 1
222

223
// ─── sd-bus types (no systemd headers needed) ───────────────────────────────
224

225
struct sd_bus;
226
struct sd_bus_message;
227
struct sd_bus_slot;
228

229
struct sd_bus_error
230
{
231
  const char* name;
232
  const char* message;
233
  int _need_free;
234
};
235

236
using sd_bus_message_handler_t
237
    = int (*)(sd_bus_message*, void*, sd_bus_error*);
238

239
} // extern "C"
240

241
// ─── SPA pod builder (minimal, raw bytes) ────────────────────────────────────
242

243
namespace
244
{
245

246
struct PodBuilder
×
247
{
248
  uint8_t buf[1024]{};
×
249
  uint32_t offset{0};
×
250

251
  void write_bytes(const void* data, uint32_t len)
×
252
  {
253
    std::memcpy(buf + offset, data, len);
×
254
    offset += len;
×
255
  }
×
256

257
  void write_u32(uint32_t v) { write_bytes(&v, 4); }
×
258

259
  void write_pod(uint32_t size, uint32_t type)
×
260
  {
261
    write_u32(size);
×
262
    write_u32(type);
×
263
  }
×
264

265
  void write_id(uint32_t value)
×
266
  {
267
    write_pod(4, SPA_TYPE_Id);
×
268
    write_u32(value);
×
269
  }
×
270

271
  void write_prop(uint32_t key, uint32_t flags)
×
272
  {
273
    write_u32(key);
×
274
    write_u32(flags);
×
275
  }
×
276

277
  // Build the EnumFormat object pod for video capture.
278
  // Specifies media type=video, subtype=raw, and lets PipeWire
279
  // negotiate format, size, and framerate from the portal source.
280
  spa_pod* buildEnumFormat()
×
281
  {
282
    offset = 0;
×
283
    uint32_t objStart = offset;
×
284

285
    // Object pod header (placeholder size, type)
286
    write_pod(0, SPA_TYPE_OBJECT_Format);
×
287
    // Object body: type, id
288
    write_u32(SPA_TYPE_OBJECT_Format);
×
289
    write_u32(SPA_PARAM_EnumFormat);
×
290

291
    // Property: mediaType = video
292
    write_prop(SPA_FORMAT_mediaType, 0);
×
293
    write_id(SPA_MEDIA_TYPE_video);
×
294

295
    // Property: mediaSubtype = raw
296
    write_prop(SPA_FORMAT_mediaSubtype, 0);
×
297
    write_id(SPA_MEDIA_SUBTYPE_raw);
×
298

299
    // No further constraints: the portal source provides its own format.
300

301
    // Patch the object pod size (excludes the 8-byte pod header)
302
    uint32_t totalSize = offset - objStart - 8;
×
303
    std::memcpy(buf + objStart, &totalSize, 4);
×
304

305
    return reinterpret_cast<spa_pod*>(buf + objStart);
×
306
  }
307
};
308

309
} // anonymous namespace
310

311
// ─── PipeWire symbol loader ──────────────────────────────────────────────────
312

313
namespace Gfx::WindowCapture
314
{
315

316
// Forward declaration from the X11 backend
317
#if defined(HAS_X11_WINDOW_CAPTURE)
318
std::unique_ptr<WindowCaptureBackend> createX11Backend();
319
#endif
320

321
// Function pointer types for the PipeWire C API (no headers needed)
322
extern "C"
323
{
324
using pw_init_t = void (*)(int* argc, char*** argv);
325
using pw_deinit_t = void (*)();
326

327
using pw_thread_loop_new_t
328
    = pw_thread_loop* (*)(const char* name, const void* props);
329
using pw_thread_loop_destroy_t = void (*)(pw_thread_loop*);
330
using pw_thread_loop_start_t = int (*)(pw_thread_loop*);
331
using pw_thread_loop_stop_t = void (*)(pw_thread_loop*);
332
using pw_thread_loop_get_loop_t = pw_loop* (*)(pw_thread_loop*);
333
using pw_thread_loop_lock_t = void (*)(pw_thread_loop*);
334
using pw_thread_loop_unlock_t = void (*)(pw_thread_loop*);
335
using pw_thread_loop_signal_t = void (*)(pw_thread_loop*, bool);
336
using pw_thread_loop_wait_t = void (*)(pw_thread_loop*);
337

338
using pw_context_new_t
339
    = pw_context* (*)(pw_loop*, pw_properties*, size_t);
340
using pw_context_destroy_t = void (*)(pw_context*);
341
using pw_context_connect_fd_t
342
    = pw_core* (*)(pw_context*, int fd, pw_properties*, size_t);
343

344
using pw_core_disconnect_t = int (*)(pw_core*);
345

346
using pw_stream_new_t
347
    = pw_stream* (*)(pw_core*, const char* name, pw_properties*);
348
using pw_stream_destroy_t = void (*)(pw_stream*);
349
using pw_stream_connect_t = int (*)(
350
    pw_stream*, enum pw_direction, uint32_t target_id,
351
    enum pw_stream_flags flags, const spa_pod** params,
352
    uint32_t n_params);
353
using pw_stream_disconnect_t = int (*)(pw_stream*);
354
using pw_stream_dequeue_buffer_t = pw_buffer* (*)(pw_stream*);
355
using pw_stream_queue_buffer_t = int (*)(pw_stream*, pw_buffer*);
356
using pw_stream_add_listener_t = void (*)(
357
    pw_stream*, spa_hook*, const pw_stream_events*, void*);
358
using pw_stream_set_active_t = int (*)(pw_stream*, bool);
359

360
using pw_properties_new_t = pw_properties* (*)(const char* key, ...);
361
using pw_properties_free_t = void (*)(pw_properties*);
362
}
363

364
struct libpipewire_capture
365
{
UNCOV
366
  pw_init_t init{};
×
367
  pw_deinit_t deinit{};
×
368

369
  pw_thread_loop_new_t thread_loop_new{};
×
370
  pw_thread_loop_destroy_t thread_loop_destroy{};
×
371
  pw_thread_loop_start_t thread_loop_start{};
×
372
  pw_thread_loop_stop_t thread_loop_stop{};
×
373
  pw_thread_loop_get_loop_t thread_loop_get_loop{};
×
374
  pw_thread_loop_lock_t thread_loop_lock{};
×
375
  pw_thread_loop_unlock_t thread_loop_unlock{};
×
UNCOV
376
  pw_thread_loop_signal_t thread_loop_signal{};
×
377
  pw_thread_loop_wait_t thread_loop_wait{};
×
378

379
  pw_context_new_t context_new{};
×
UNCOV
380
  pw_context_destroy_t context_destroy{};
×
381
  pw_context_connect_fd_t context_connect_fd{};
×
382

383
  pw_core_disconnect_t core_disconnect{};
×
384

385
  pw_stream_new_t stream_new{};
×
386
  pw_stream_destroy_t stream_destroy{};
×
387
  pw_stream_connect_t stream_connect{};
×
388
  pw_stream_disconnect_t stream_disconnect{};
×
389
  pw_stream_dequeue_buffer_t stream_dequeue_buffer{};
×
390
  pw_stream_queue_buffer_t stream_queue_buffer{};
×
UNCOV
391
  pw_stream_add_listener_t stream_add_listener{};
×
392
  pw_stream_set_active_t stream_set_active{};
×
393

UNCOV
394
  pw_properties_new_t properties_new{};
×
395
  pw_properties_free_t properties_free{};
×
396

397
  bool available{};
×
398

399
  static const libpipewire_capture& instance()
×
400
  {
401
    static const libpipewire_capture self;
×
UNCOV
402
    return self;
×
UNCOV
403
  }
×
404

405
private:
406
  ossia::dylib_loader m_lib;
407

408
  template <typename T>
409
  T sym(const char* name)
×
410
  {
UNCOV
411
    return m_lib.symbol<T>(name);
×
412
  }
413

414
  libpipewire_capture()
×
415
  try
UNCOV
416
    : m_lib{std::vector<std::string_view>{
×
417
          "libpipewire-0.3.so.0", "libpipewire-0.3.so"}}
418
  {
UNCOV
419
    init = sym<pw_init_t>("pw_init");
×
420
    deinit = sym<pw_deinit_t>("pw_deinit");
×
421

422
    thread_loop_new = sym<pw_thread_loop_new_t>("pw_thread_loop_new");
×
423
    thread_loop_destroy
×
424
        = sym<pw_thread_loop_destroy_t>("pw_thread_loop_destroy");
×
425
    thread_loop_start
×
426
        = sym<pw_thread_loop_start_t>("pw_thread_loop_start");
×
427
    thread_loop_stop
×
428
        = sym<pw_thread_loop_stop_t>("pw_thread_loop_stop");
×
429
    thread_loop_get_loop
×
430
        = sym<pw_thread_loop_get_loop_t>("pw_thread_loop_get_loop");
×
431
    thread_loop_lock
×
432
        = sym<pw_thread_loop_lock_t>("pw_thread_loop_lock");
×
433
    thread_loop_unlock
×
434
        = sym<pw_thread_loop_unlock_t>("pw_thread_loop_unlock");
×
435
    thread_loop_signal
×
436
        = sym<pw_thread_loop_signal_t>("pw_thread_loop_signal");
×
UNCOV
437
    thread_loop_wait
×
438
        = sym<pw_thread_loop_wait_t>("pw_thread_loop_wait");
×
439

440
    context_new = sym<pw_context_new_t>("pw_context_new");
×
441
    context_destroy = sym<pw_context_destroy_t>("pw_context_destroy");
×
UNCOV
442
    context_connect_fd
×
443
        = sym<pw_context_connect_fd_t>("pw_context_connect_fd");
×
444

445
    core_disconnect = sym<pw_core_disconnect_t>("pw_core_disconnect");
×
446

447
    stream_new = sym<pw_stream_new_t>("pw_stream_new");
×
448
    stream_destroy = sym<pw_stream_destroy_t>("pw_stream_destroy");
×
449
    stream_connect = sym<pw_stream_connect_t>("pw_stream_connect");
×
450
    stream_disconnect
×
451
        = sym<pw_stream_disconnect_t>("pw_stream_disconnect");
×
452
    stream_dequeue_buffer
×
453
        = sym<pw_stream_dequeue_buffer_t>("pw_stream_dequeue_buffer");
×
454
    stream_queue_buffer
×
455
        = sym<pw_stream_queue_buffer_t>("pw_stream_queue_buffer");
×
456
    stream_add_listener
×
457
        = sym<pw_stream_add_listener_t>("pw_stream_add_listener");
×
UNCOV
458
    stream_set_active
×
459
        = sym<pw_stream_set_active_t>("pw_stream_set_active");
×
460

UNCOV
461
    properties_new = sym<pw_properties_new_t>("pw_properties_new");
×
462
    properties_free = sym<pw_properties_free_t>("pw_properties_free");
×
463

464
    available = init && deinit && thread_loop_new && thread_loop_destroy
×
465
                && thread_loop_start && thread_loop_stop
×
466
                && thread_loop_get_loop && thread_loop_lock
×
467
                && thread_loop_unlock && context_new && context_destroy
×
468
                && context_connect_fd && core_disconnect && stream_new
×
469
                && stream_destroy && stream_connect && stream_disconnect
×
470
                && stream_dequeue_buffer && stream_queue_buffer
×
471
                && stream_add_listener && properties_new
×
UNCOV
472
                && properties_free;
×
UNCOV
473
  }
×
474
  catch(...)
475
  {
UNCOV
476
    available = false;
×
UNCOV
477
  }
×
478
};
479

480
// ─── sd-bus symbol loader ───────────────────────────────────────────────────
481

482
extern "C"
483
{
484
using sd_bus_open_user_t = int (*)(sd_bus**);
485
using sd_bus_unref_t = sd_bus* (*)(sd_bus*);
486
using sd_bus_get_unique_name_t = int (*)(sd_bus*, const char**);
487
using sd_bus_call_t = int (*)(
488
    sd_bus*, sd_bus_message*, uint64_t usec, sd_bus_error*,
489
    sd_bus_message**);
490
using sd_bus_message_new_method_call_t = int (*)(
491
    sd_bus*, sd_bus_message**, const char*, const char*, const char*,
492
    const char*);
493
using sd_bus_message_unref_t = sd_bus_message* (*)(sd_bus_message*);
494
using sd_bus_message_append_basic_t
495
    = int (*)(sd_bus_message*, char, const void*);
496
using sd_bus_message_open_container_t
497
    = int (*)(sd_bus_message*, char, const char*);
498
using sd_bus_message_close_container_t = int (*)(sd_bus_message*);
499
using sd_bus_message_read_basic_t
500
    = int (*)(sd_bus_message*, char, void*);
501
using sd_bus_message_enter_container_t
502
    = int (*)(sd_bus_message*, char, const char*);
503
using sd_bus_message_exit_container_t = int (*)(sd_bus_message*);
504
using sd_bus_message_skip_t = int (*)(sd_bus_message*, const char*);
505
using sd_bus_add_match_t = int (*)(
506
    sd_bus*, sd_bus_slot**, const char*, sd_bus_message_handler_t,
507
    void*);
508
using sd_bus_slot_unref_t = sd_bus_slot* (*)(sd_bus_slot*);
509
using sd_bus_process_t = int (*)(sd_bus*, sd_bus_message**);
510
using sd_bus_wait_t = int (*)(sd_bus*, uint64_t);
511
using sd_bus_error_free_t = void (*)(sd_bus_error*);
512
}
513

514
struct libsdbus
515
{
516
  sd_bus_open_user_t open_user{};
×
517
  sd_bus_unref_t unref{};
×
518
  sd_bus_get_unique_name_t get_unique_name{};
×
519
  sd_bus_call_t call{};
×
520
  sd_bus_message_new_method_call_t message_new_method_call{};
×
521
  sd_bus_message_unref_t message_unref{};
×
522
  sd_bus_message_append_basic_t message_append_basic{};
×
523
  sd_bus_message_open_container_t message_open_container{};
×
524
  sd_bus_message_close_container_t message_close_container{};
×
525
  sd_bus_message_read_basic_t message_read_basic{};
×
526
  sd_bus_message_enter_container_t message_enter_container{};
×
527
  sd_bus_message_exit_container_t message_exit_container{};
×
528
  sd_bus_message_skip_t message_skip{};
×
529
  sd_bus_add_match_t add_match{};
×
530
  sd_bus_slot_unref_t slot_unref{};
×
531
  sd_bus_process_t process{};
×
UNCOV
532
  sd_bus_wait_t wait{};
×
533
  sd_bus_error_free_t error_free{};
×
534

535
  bool available{};
×
536

537
  static const libsdbus& instance()
×
538
  {
539
    static const libsdbus self;
×
UNCOV
540
    return self;
×
UNCOV
541
  }
×
542

543
private:
544
  ossia::dylib_loader m_lib;
545

546
  template <typename T>
547
  T sym(const char* name)
×
548
  {
UNCOV
549
    return m_lib.symbol<T>(name);
×
550
  }
551

552
  libsdbus()
×
553
  try
UNCOV
554
    : m_lib{std::vector<std::string_view>{
×
555
          "libsystemd.so.0", "libsystemd.so"}}
556
  {
557
    open_user = sym<sd_bus_open_user_t>("sd_bus_open_user");
×
558
    unref = sym<sd_bus_unref_t>("sd_bus_unref");
×
559
    get_unique_name
×
560
        = sym<sd_bus_get_unique_name_t>("sd_bus_get_unique_name");
×
UNCOV
561
    call = sym<sd_bus_call_t>("sd_bus_call");
×
562
    message_new_method_call = sym<sd_bus_message_new_method_call_t>(
×
563
        "sd_bus_message_new_method_call");
564
    message_unref
×
UNCOV
565
        = sym<sd_bus_message_unref_t>("sd_bus_message_unref");
×
566
    message_append_basic = sym<sd_bus_message_append_basic_t>(
×
567
        "sd_bus_message_append_basic");
568
    message_open_container = sym<sd_bus_message_open_container_t>(
×
569
        "sd_bus_message_open_container");
570
    message_close_container = sym<sd_bus_message_close_container_t>(
×
571
        "sd_bus_message_close_container");
572
    message_read_basic = sym<sd_bus_message_read_basic_t>(
×
573
        "sd_bus_message_read_basic");
574
    message_enter_container = sym<sd_bus_message_enter_container_t>(
×
575
        "sd_bus_message_enter_container");
576
    message_exit_container = sym<sd_bus_message_exit_container_t>(
×
577
        "sd_bus_message_exit_container");
578
    message_skip
×
579
        = sym<sd_bus_message_skip_t>("sd_bus_message_skip");
×
580
    add_match = sym<sd_bus_add_match_t>("sd_bus_add_match");
×
581
    slot_unref = sym<sd_bus_slot_unref_t>("sd_bus_slot_unref");
×
582
    process = sym<sd_bus_process_t>("sd_bus_process");
×
UNCOV
583
    wait = sym<sd_bus_wait_t>("sd_bus_wait");
×
584
    error_free = sym<sd_bus_error_free_t>("sd_bus_error_free");
×
585

586
    available = open_user && unref && get_unique_name && call
×
587
                && message_new_method_call && message_unref
×
588
                && message_append_basic && message_open_container
×
589
                && message_close_container && message_read_basic
×
590
                && message_enter_container && message_exit_container
×
591
                && message_skip && add_match && slot_unref && process
×
UNCOV
592
                && wait && error_free;
×
UNCOV
593
  }
×
594
  catch(...)
595
  {
UNCOV
596
    available = false;
×
UNCOV
597
  }
×
598
};
599

600
// ─── xdg-desktop-portal helpers (sd-bus) ────────────────────────────────────
601

602
namespace
603
{
604

UNCOV
605
static std::string makeToken()
×
606
{
607
  static std::atomic<int> counter{0};
608
  return "ossia_score_"
×
609
         + std::to_string(static_cast<unsigned>(getpid())) + "_"
×
UNCOV
610
         + std::to_string(counter.fetch_add(1));
×
UNCOV
611
}
×
612

613
// Helper: append a string→string entry into an open a{sv} container
UNCOV
614
static void appendDictString(
×
615
    const libsdbus& sd, sd_bus_message* m, const char* key,
616
    const char* value)
617
{
618
  sd.message_open_container(m, 'e', "sv");
×
619
  sd.message_append_basic(m, 's', key);
×
620
  sd.message_open_container(m, 'v', "s");
×
621
  sd.message_append_basic(m, 's', value);
×
622
  sd.message_close_container(m);
×
UNCOV
623
  sd.message_close_container(m);
×
UNCOV
624
}
×
625

626
// Helper: append a string→uint32 entry into an open a{sv} container
UNCOV
627
static void appendDictUint32(
×
628
    const libsdbus& sd, sd_bus_message* m, const char* key,
629
    uint32_t value)
630
{
631
  sd.message_open_container(m, 'e', "sv");
×
632
  sd.message_append_basic(m, 's', key);
×
633
  sd.message_open_container(m, 'v', "u");
×
634
  sd.message_append_basic(m, 'u', &value);
×
635
  sd.message_close_container(m);
×
UNCOV
636
  sd.message_close_container(m);
×
UNCOV
637
}
×
638

639
// Helper: append a string→bool entry into an open a{sv} container
UNCOV
640
static void appendDictBool(
×
641
    const libsdbus& sd, sd_bus_message* m, const char* key, bool value)
642
{
643
  sd.message_open_container(m, 'e', "sv");
×
644
  sd.message_append_basic(m, 's', key);
×
645
  sd.message_open_container(m, 'v', "b");
×
646
  int v = value ? 1 : 0;
×
647
  sd.message_append_basic(m, 'b', &v);
×
648
  sd.message_close_container(m);
×
UNCOV
649
  sd.message_close_container(m);
×
UNCOV
650
}
×
651

652
// Data passed to the Response signal callback
653
struct PortalResponse
654
{
655
  const libsdbus* sd{};
656
  uint32_t code{99};
657
  std::string session_handle;
658
  uint32_t pipewire_node{0}; // PipeWire node ID from Start response
659
  bool received{false};
660
};
661

662
// sd-bus callback for the Response(u, a{sv}) signal
UNCOV
663
static int onPortalResponse(
×
664
    sd_bus_message* m, void* userdata, sd_bus_error*)
665
{
UNCOV
666
  auto* resp = static_cast<PortalResponse*>(userdata);
×
UNCOV
667
  auto& sd = *resp->sd;
×
668

669
  // Read response code
UNCOV
670
  sd.message_read_basic(m, 'u', &resp->code);
×
671

672
  // Read results dict a{sv}
673
  if(sd.message_enter_container(m, 'a', "{sv}") >= 0)
×
674
  {
675
    while(sd.message_enter_container(m, 'e', "sv") > 0)
×
676
    {
UNCOV
677
      const char* key = nullptr;
×
678
      sd.message_read_basic(m, 's', &key);
×
679

680
      if(key && std::strcmp(key, "session_handle") == 0)
×
681
      {
682
        if(sd.message_enter_container(m, 'v', "s") >= 0)
×
683
        {
684
          const char* val = nullptr;
×
685
          sd.message_read_basic(m, 's', &val);
×
686
          if(val)
×
687
            resp->session_handle = val;
×
688
          sd.message_exit_container(m);
×
689
        }
×
UNCOV
690
      }
×
UNCOV
691
      else if(key && std::strcmp(key, "streams") == 0)
×
692
      {
693
        // streams: a(ua{sv}) — array of structs with node_id + properties
694
        // We read the first stream's node ID.
695
        if(sd.message_enter_container(m, 'v', "a(ua{sv})") >= 0)
×
696
        {
697
          if(sd.message_enter_container(m, 'a', "(ua{sv})") >= 0)
×
698
          {
699
            if(sd.message_enter_container(m, 'r', "ua{sv}") >= 0)
×
700
            {
701
              sd.message_read_basic(m, 'u', &resp->pipewire_node);
×
702
              sd.message_skip(m, "a{sv}"); // skip properties
×
703
              sd.message_exit_container(m);
×
704
            }
×
705
            sd.message_exit_container(m);
×
706
          }
×
707
          sd.message_exit_container(m);
×
UNCOV
708
        }
×
UNCOV
709
      }
×
710
      else
711
      {
712
        sd.message_skip(m, "v");
×
713
      }
714
      sd.message_exit_container(m);
×
715
    }
UNCOV
716
    sd.message_exit_container(m);
×
717
  }
×
718

UNCOV
719
  resp->received = true;
×
UNCOV
720
  return 0;
×
721
}
722

723
// Wait for a Response signal on the bus, with timeout.
UNCOV
724
static bool waitForResponse(
×
725
    const libsdbus& sd, sd_bus* bus, PortalResponse& resp,
726
    int timeoutMs)
727
{
UNCOV
728
  auto deadline = std::chrono::steady_clock::now()
×
729
                  + std::chrono::milliseconds(timeoutMs);
×
730

731
  while(!resp.received)
×
732
  {
733
    auto now = std::chrono::steady_clock::now();
×
UNCOV
734
    if(now >= deadline)
×
735
      break;
×
736

737
    int r = sd.process(bus, nullptr);
×
738
    if(r < 0)
×
739
      break;
×
UNCOV
740
    if(r > 0)
×
741
      continue; // more work queued
×
742

743
    int64_t remaining_us
×
744
        = std::chrono::duration_cast<std::chrono::microseconds>(
×
745
              deadline - now)
×
746
              .count();
×
747
    if(remaining_us > 100000)
×
UNCOV
748
      remaining_us = 100000;
×
UNCOV
749
    sd.wait(bus, static_cast<uint64_t>(remaining_us));
×
750
  }
751

UNCOV
752
  return resp.received;
×
753
}
754

755
// Build the request object path for the portal Response signal.
UNCOV
756
static std::string makeRequestPath(
×
757
    const libsdbus& sd, sd_bus* bus, const std::string& token)
758
{
759
  const char* uniqueName = nullptr;
×
760
  sd.get_unique_name(bus, &uniqueName);
×
UNCOV
761
  if(!uniqueName)
×
UNCOV
762
    return {};
×
763

764
  // Skip leading ':' and replace '.' with '_'
765
  std::string sender(uniqueName + 1);
×
766
  for(auto& c : sender)
×
UNCOV
767
    if(c == '.')
×
768
      c = '_';
×
769

770
  return "/org/freedesktop/portal/desktop/request/" + sender + "/"
×
UNCOV
771
         + token;
×
UNCOV
772
}
×
773

774
// Portal source types
775
static constexpr uint32_t PORTAL_SOURCE_MONITOR = 1;
776
static constexpr uint32_t PORTAL_SOURCE_WINDOW = 2;
777

778
// Portal cursor modes
779
static constexpr uint32_t PORTAL_CURSOR_HIDDEN = 1;
780
static constexpr uint32_t PORTAL_CURSOR_EMBEDDED = 2;
781
static constexpr uint32_t PORTAL_CURSOR_METADATA = 4;
782

783
static constexpr const char* PORTAL_DEST
784
    = "org.freedesktop.portal.Desktop";
785
static constexpr const char* PORTAL_PATH
786
    = "/org/freedesktop/portal/desktop";
787
static constexpr const char* PORTAL_SCREENCAST_IFACE
788
    = "org.freedesktop.portal.ScreenCast";
789

790
// Result of the portal ScreenCast flow
791
struct PortalScreenCastResult
×
792
{
UNCOV
793
  int fd{-1};
×
UNCOV
794
  uint32_t pipewire_node{0};
×
795
};
796

797
// Perform the full portal ScreenCast flow and return the PipeWire fd + node ID.
798
// sourceType: 1=MONITOR, 2=WINDOW
799
static PortalScreenCastResult openPortalScreenCast(uint32_t sourceType)
×
800
{
801
  auto& sd = libsdbus::instance();
×
UNCOV
802
  if(!sd.available)
×
803
    return {};
×
804

UNCOV
805
  sd_bus* bus = nullptr;
×
806
  if(sd.open_user(&bus) < 0 || !bus)
×
807
  {
UNCOV
808
    qDebug() << "WindowCapture PipeWire: sd_bus_open_user failed";
×
UNCOV
809
    return {};
×
810
  }
811

UNCOV
812
  PortalScreenCastResult result;
×
UNCOV
813
  sd_bus_error error{nullptr, nullptr, 0};
×
814

815
  // ── Step 1: CreateSession ──
816
  std::string sessionToken = makeToken();
×
UNCOV
817
  std::string requestToken = makeToken();
×
818
  std::string requestPath = makeRequestPath(sd, bus, requestToken);
×
819

UNCOV
820
  PortalResponse resp{&sd, {}, {}, {}, false};
×
UNCOV
821
  sd_bus_slot* slot = nullptr;
×
822

823
  {
824
    std::string matchRule
825
        = "type='signal',interface='org.freedesktop.portal.Request'"
×
826
          ",member='Response',path='"
827
          + requestPath + "'";
×
UNCOV
828
    sd.add_match(bus, &slot, matchRule.c_str(), onPortalResponse, &resp);
×
UNCOV
829
  }
×
830

831
  {
832
    sd_bus_message* msg = nullptr;
×
UNCOV
833
    sd.message_new_method_call(
×
834
        bus, &msg, PORTAL_DEST, PORTAL_PATH, PORTAL_SCREENCAST_IFACE,
×
835
        "CreateSession");
836
    sd.message_open_container(msg, 'a', "{sv}");
×
837
    appendDictString(sd, msg, "handle_token", requestToken.c_str());
×
838
    appendDictString(
×
UNCOV
839
        sd, msg, "session_handle_token", sessionToken.c_str());
×
840
    sd.message_close_container(msg);
×
841

842
    sd_bus_message* reply = nullptr;
×
843
    int r = sd.call(bus, msg, 0, &error, &reply);
×
844
    sd.message_unref(msg);
×
UNCOV
845
    if(reply)
×
846
      sd.message_unref(reply);
×
847

848
    if(r < 0)
×
849
    {
850
      qDebug() << "WindowCapture PipeWire: CreateSession call failed:"
×
851
               << (error.message ? error.message : "unknown");
×
852
      sd.error_free(&error);
×
853
      sd.slot_unref(slot);
×
UNCOV
854
      sd.unref(bus);
×
UNCOV
855
      return {};
×
856
    }
857
  }
858

859
  if(!waitForResponse(sd, bus, resp, 30000) || resp.code != 0)
×
860
  {
861
    qDebug() << "WindowCapture PipeWire: CreateSession rejected or timed out";
×
862
    sd.slot_unref(slot);
×
UNCOV
863
    sd.unref(bus);
×
UNCOV
864
    return {};
×
865
  }
866

867
  std::string sessionHandle = resp.session_handle;
×
UNCOV
868
  sd.slot_unref(slot);
×
869
  slot = nullptr;
×
870

871
  if(sessionHandle.empty())
×
872
  {
873
    qDebug() << "WindowCapture PipeWire: no session handle in response";
×
UNCOV
874
    sd.unref(bus);
×
UNCOV
875
    return {};
×
876
  }
877

878
  // ── Step 2: SelectSources ──
879
  requestToken = makeToken();
×
UNCOV
880
  requestPath = makeRequestPath(sd, bus, requestToken);
×
UNCOV
881
  resp = PortalResponse{&sd, {}, {}, {}, false};
×
882

883
  {
884
    std::string matchRule
885
        = "type='signal',interface='org.freedesktop.portal.Request'"
×
886
          ",member='Response',path='"
887
          + requestPath + "'";
×
UNCOV
888
    sd.add_match(bus, &slot, matchRule.c_str(), onPortalResponse, &resp);
×
UNCOV
889
  }
×
890

891
  {
892
    sd_bus_message* msg = nullptr;
×
UNCOV
893
    sd.message_new_method_call(
×
894
        bus, &msg, PORTAL_DEST, PORTAL_PATH, PORTAL_SCREENCAST_IFACE,
×
895
        "SelectSources");
896
    sd.message_append_basic(msg, 'o', sessionHandle.c_str());
×
897
    sd.message_open_container(msg, 'a', "{sv}");
×
898
    appendDictString(sd, msg, "handle_token", requestToken.c_str());
×
899
    appendDictUint32(sd, msg, "types", sourceType);
×
900
    appendDictBool(sd, msg, "multiple", false);
×
UNCOV
901
    appendDictUint32(sd, msg, "cursor_mode", PORTAL_CURSOR_EMBEDDED);
×
902
    sd.message_close_container(msg);
×
903

904
    sd_bus_message* reply = nullptr;
×
905
    int r = sd.call(bus, msg, 0, &error, &reply);
×
906
    sd.message_unref(msg);
×
UNCOV
907
    if(reply)
×
908
      sd.message_unref(reply);
×
909

910
    if(r < 0)
×
911
    {
912
      qDebug() << "WindowCapture PipeWire: SelectSources call failed:"
×
913
               << (error.message ? error.message : "unknown");
×
914
      sd.error_free(&error);
×
915
      sd.slot_unref(slot);
×
UNCOV
916
      sd.unref(bus);
×
UNCOV
917
      return {};
×
918
    }
919
  }
920

921
  if(!waitForResponse(sd, bus, resp, 60000) || resp.code != 0)
×
922
  {
923
    qDebug() << "WindowCapture PipeWire: SelectSources rejected or timed out";
×
924
    sd.slot_unref(slot);
×
UNCOV
925
    sd.unref(bus);
×
926
    return {};
×
927
  }
UNCOV
928
  sd.slot_unref(slot);
×
UNCOV
929
  slot = nullptr;
×
930

931
  // ── Step 3: Start (user confirms the picker) ──
932
  requestToken = makeToken();
×
UNCOV
933
  requestPath = makeRequestPath(sd, bus, requestToken);
×
UNCOV
934
  resp = PortalResponse{&sd, {}, {}, {}, false};
×
935

936
  {
937
    std::string matchRule
938
        = "type='signal',interface='org.freedesktop.portal.Request'"
×
939
          ",member='Response',path='"
940
          + requestPath + "'";
×
UNCOV
941
    sd.add_match(bus, &slot, matchRule.c_str(), onPortalResponse, &resp);
×
UNCOV
942
  }
×
943

944
  {
945
    sd_bus_message* msg = nullptr;
×
UNCOV
946
    sd.message_new_method_call(
×
947
        bus, &msg, PORTAL_DEST, PORTAL_PATH, PORTAL_SCREENCAST_IFACE,
×
948
        "Start");
949
    sd.message_append_basic(msg, 'o', sessionHandle.c_str());
×
950
    sd.message_append_basic(msg, 's', ""); // parent window identifier
×
951
    sd.message_open_container(msg, 'a', "{sv}");
×
UNCOV
952
    appendDictString(sd, msg, "handle_token", requestToken.c_str());
×
953
    sd.message_close_container(msg);
×
954

955
    sd_bus_message* reply = nullptr;
×
956
    int r = sd.call(bus, msg, 0, &error, &reply);
×
957
    sd.message_unref(msg);
×
UNCOV
958
    if(reply)
×
959
      sd.message_unref(reply);
×
960

961
    if(r < 0)
×
962
    {
963
      qDebug() << "WindowCapture PipeWire: Start call failed:"
×
964
               << (error.message ? error.message : "unknown");
×
965
      sd.error_free(&error);
×
966
      sd.slot_unref(slot);
×
UNCOV
967
      sd.unref(bus);
×
UNCOV
968
      return {};
×
969
    }
970
  }
971

972
  if(!waitForResponse(sd, bus, resp, 60000) || resp.code != 0)
×
973
  {
974
    qDebug() << "WindowCapture PipeWire: Start rejected or timed out";
×
975
    sd.slot_unref(slot);
×
UNCOV
976
    sd.unref(bus);
×
UNCOV
977
    return {};
×
978
  }
979

980
  // Extract the PipeWire node ID from the Start response
UNCOV
981
  result.pipewire_node = resp.pipewire_node;
×
982
  qDebug() << "WindowCapture PipeWire: got node id:" << result.pipewire_node;
×
983

UNCOV
984
  sd.slot_unref(slot);
×
UNCOV
985
  slot = nullptr;
×
986

987
  // ── Step 4: OpenPipeWireRemote ──
988
  {
989
    sd_bus_message* msg = nullptr;
×
UNCOV
990
    sd.message_new_method_call(
×
991
        bus, &msg, PORTAL_DEST, PORTAL_PATH, PORTAL_SCREENCAST_IFACE,
×
992
        "OpenPipeWireRemote");
993
    sd.message_append_basic(msg, 'o', sessionHandle.c_str());
×
UNCOV
994
    sd.message_open_container(msg, 'a', "{sv}");
×
995
    sd.message_close_container(msg);
×
996

997
    sd_bus_message* reply = nullptr;
×
UNCOV
998
    int r = sd.call(bus, msg, 0, &error, &reply);
×
999
    sd.message_unref(msg);
×
1000

1001
    if(r < 0 || !reply)
×
1002
    {
1003
      qDebug() << "WindowCapture PipeWire: OpenPipeWireRemote failed:"
×
1004
               << (error.message ? error.message : "unknown");
×
1005
      sd.error_free(&error);
×
1006
      if(reply)
×
1007
        sd.message_unref(reply);
×
UNCOV
1008
      sd.unref(bus);
×
UNCOV
1009
      return {};
×
1010
    }
1011

1012
    int fd = -1;
×
UNCOV
1013
    sd.message_read_basic(reply, 'h', &fd);
×
1014
    sd.message_unref(reply);
×
1015

1016
    if(fd < 0)
×
1017
    {
1018
      qDebug() << "WindowCapture PipeWire: invalid fd from OpenPipeWireRemote";
×
UNCOV
1019
      sd.unref(bus);
×
UNCOV
1020
      return {};
×
1021
    }
1022

1023
    // dup() so the fd outlives the bus connection
UNCOV
1024
    result.fd = ::dup(fd);
×
1025
  }
1026

1027
  sd.unref(bus);
×
UNCOV
1028
  return result;
×
1029
}
×
1030

1031
static bool portalAvailable()
×
1032
{
1033
  auto& sd = libsdbus::instance();
×
UNCOV
1034
  if(!sd.available)
×
1035
    return false;
×
1036

1037
  sd_bus* bus = nullptr;
×
UNCOV
1038
  if(sd.open_user(&bus) < 0 || !bus)
×
UNCOV
1039
    return false;
×
1040

1041
  // Try to introspect the portal — just create a method call to verify it exists
1042
  sd_bus_message* msg = nullptr;
×
UNCOV
1043
  int r = sd.message_new_method_call(
×
1044
      bus, &msg, PORTAL_DEST, PORTAL_PATH,
×
1045
      "org.freedesktop.DBus.Properties", "Get");
1046
  if(r >= 0 && msg)
×
1047
  {
UNCOV
1048
    sd.message_append_basic(msg, 's', PORTAL_SCREENCAST_IFACE);
×
1049
    sd.message_append_basic(msg, 's', "AvailableSourceTypes");
×
1050

1051
    sd_bus_error error{nullptr, nullptr, 0};
×
1052
    sd_bus_message* reply = nullptr;
×
1053
    r = sd.call(bus, msg, 5000000, &error, &reply); // 5s timeout
×
1054
    sd.message_unref(msg);
×
1055
    if(reply)
×
UNCOV
1056
      sd.message_unref(reply);
×
1057
    sd.error_free(&error);
×
1058

UNCOV
1059
    sd.unref(bus);
×
UNCOV
1060
    return r >= 0;
×
1061
  }
1062

1063
  sd.unref(bus);
×
UNCOV
1064
  return false;
×
UNCOV
1065
}
×
1066

1067
} // anonymous namespace
1068

1069
// ─── PipeWire Window Capture Backend ─────────────────────────────────────────
1070

UNCOV
1071
class PipeWireWindowCaptureBackend final : public WindowCaptureBackend
×
1072
{
1073
public:
1074
  ~PipeWireWindowCaptureBackend() override { stop(); }
×
1075

1076
  bool available() const override
×
1077
  {
UNCOV
1078
    return libpipewire_capture::instance().available && portalAvailable();
×
1079
  }
1080

1081
  bool supportsMode(CaptureMode mode) const override
×
1082
  {
UNCOV
1083
    switch(mode)
×
1084
    {
1085
      case CaptureMode::Window:
1086
      case CaptureMode::SingleScreen:
1087
      case CaptureMode::AllScreens:
1088
        return true;
×
1089
      case CaptureMode::Region:
1090
        return false;
×
1091
    }
UNCOV
1092
    return false;
×
1093
  }
×
1094

UNCOV
1095
  std::vector<CapturableWindow> enumerate() override
×
1096
  {
1097
    // Wayland does not allow window enumeration.
1098
    // The portal picker dialog handles selection during start().
UNCOV
1099
    return {};
×
1100
  }
1101

UNCOV
1102
  std::vector<CapturableScreen> enumerateScreens() override
×
1103
  {
1104
    // Wayland does not allow screen enumeration.
1105
    // The portal picker dialog handles selection during start().
UNCOV
1106
    return {};
×
1107
  }
1108

1109
  bool start(const CaptureTarget& target) override
×
1110
  {
1111
    stop();
×
1112

1113
    auto& pw = libpipewire_capture::instance();
×
UNCOV
1114
    if(!pw.available)
×
UNCOV
1115
      return false;
×
1116

1117
    // Initialize PipeWire
UNCOV
1118
    pw.init(nullptr, nullptr);
×
1119

1120
    // Choose portal source type based on capture mode
UNCOV
1121
    uint32_t sourceType = PORTAL_SOURCE_WINDOW;
×
UNCOV
1122
    switch(target.mode)
×
1123
    {
1124
      case CaptureMode::Window:
UNCOV
1125
        sourceType = PORTAL_SOURCE_WINDOW;
×
UNCOV
1126
        break;
×
1127
      case CaptureMode::AllScreens:
1128
      case CaptureMode::SingleScreen:
UNCOV
1129
        sourceType = PORTAL_SOURCE_MONITOR;
×
UNCOV
1130
        break;
×
1131
      case CaptureMode::Region:
1132
        // Region not supported on PipeWire
UNCOV
1133
        return false;
×
1134
    }
1135

1136
    // Run the portal flow to get a PipeWire fd and node ID.
UNCOV
1137
    auto portalResult = openPortalScreenCast(sourceType);
×
1138
    if(portalResult.fd < 0)
×
1139
    {
UNCOV
1140
      qDebug() << "WindowCapture PipeWire: portal flow failed";
×
1141
      return false;
×
1142
    }
UNCOV
1143
    int fd = portalResult.fd;
×
UNCOV
1144
    m_pipewireNode = portalResult.pipewire_node;
×
1145

1146
    // Create PipeWire thread loop
UNCOV
1147
    m_loop = pw.thread_loop_new("score-wincap", nullptr);
×
1148
    if(!m_loop)
×
1149
    {
1150
      qDebug() << "WindowCapture PipeWire: pw_thread_loop_new failed";
×
UNCOV
1151
      ::close(fd);
×
UNCOV
1152
      return false;
×
1153
    }
1154

1155
    pw_loop* loop = pw.thread_loop_get_loop(m_loop);
×
1156

UNCOV
1157
    m_context = pw.context_new(loop, nullptr, 0);
×
1158
    if(!m_context)
×
1159
    {
1160
      qDebug() << "WindowCapture PipeWire: pw_context_new failed";
×
1161
      pw.thread_loop_destroy(m_loop);
×
1162
      m_loop = nullptr;
×
UNCOV
1163
      ::close(fd);
×
UNCOV
1164
      return false;
×
1165
    }
1166

1167
    if(pw.thread_loop_start(m_loop) != 0)
×
1168
    {
1169
      qDebug() << "WindowCapture PipeWire: pw_thread_loop_start failed";
×
1170
      pw.context_destroy(m_context);
×
1171
      m_context = nullptr;
×
1172
      pw.thread_loop_destroy(m_loop);
×
1173
      m_loop = nullptr;
×
UNCOV
1174
      ::close(fd);
×
UNCOV
1175
      return false;
×
1176
    }
1177

UNCOV
1178
    pw.thread_loop_lock(m_loop);
×
1179

1180
    // Connect to PipeWire via the portal fd
UNCOV
1181
    m_core = pw.context_connect_fd(m_context, fd, nullptr, 0);
×
1182
    if(!m_core)
×
1183
    {
1184
      qDebug() << "WindowCapture PipeWire: pw_context_connect_fd failed";
×
1185
      pw.thread_loop_unlock(m_loop);
×
1186
      pw.thread_loop_stop(m_loop);
×
1187
      pw.context_destroy(m_context);
×
1188
      m_context = nullptr;
×
1189
      pw.thread_loop_destroy(m_loop);
×
UNCOV
1190
      m_loop = nullptr;
×
UNCOV
1191
      return false;
×
1192
    }
1193

1194
    // Create stream
UNCOV
1195
    pw_properties* props = pw.properties_new(
×
1196
        "media.type", "Video", "media.category", "Capture",
1197
        "media.role", "Screen", nullptr);
1198

UNCOV
1199
    m_stream = pw.stream_new(m_core, "score-window-capture", props);
×
1200
    if(!m_stream)
×
1201
    {
1202
      qDebug() << "WindowCapture PipeWire: pw_stream_new failed";
×
1203
      pw.core_disconnect(m_core);
×
1204
      m_core = nullptr;
×
1205
      pw.thread_loop_unlock(m_loop);
×
1206
      pw.thread_loop_stop(m_loop);
×
1207
      pw.context_destroy(m_context);
×
1208
      m_context = nullptr;
×
1209
      pw.thread_loop_destroy(m_loop);
×
UNCOV
1210
      m_loop = nullptr;
×
UNCOV
1211
      return false;
×
1212
    }
1213

1214
    // Set up stream events
1215
    std::memset(&m_streamEvents, 0, sizeof(m_streamEvents));
×
1216
    m_streamEvents.version = PW_VERSION_STREAM_EVENTS;
×
1217
    m_streamEvents.state_changed
×
1218
        = &PipeWireWindowCaptureBackend::onStateChanged;
×
1219
    m_streamEvents.param_changed
×
UNCOV
1220
        = &PipeWireWindowCaptureBackend::onParamChanged;
×
1221
    m_streamEvents.process = &PipeWireWindowCaptureBackend::onProcess;
×
1222

1223
    std::memset(&m_streamListener, 0, sizeof(m_streamListener));
×
UNCOV
1224
    pw.stream_add_listener(
×
UNCOV
1225
        m_stream, &m_streamListener, &m_streamEvents, this);
×
1226

1227
    // Build format params
1228
    PodBuilder podBuilder;
×
UNCOV
1229
    spa_pod* formatPod = podBuilder.buildEnumFormat();
×
UNCOV
1230
    const spa_pod* params[] = {formatPod};
×
1231

1232
    // Connect the stream as input (we consume video from the portal).
1233
    // Use the specific node ID from the portal Start response.
UNCOV
1234
    int ret = pw.stream_connect(
×
UNCOV
1235
        m_stream, PW_DIRECTION_INPUT, m_pipewireNode,
×
1236
        static_cast<pw_stream_flags>(
1237
            PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS),
1238
        params, 1);
×
1239

1240
    pw.thread_loop_unlock(m_loop);
×
1241

1242
    if(ret < 0)
×
1243
    {
1244
      qDebug() << "WindowCapture PipeWire: pw_stream_connect failed:"
×
1245
               << ret;
×
UNCOV
1246
      stop();
×
UNCOV
1247
      return false;
×
1248
    }
1249

1250
    m_running.store(true, std::memory_order_release);
×
UNCOV
1251
    return true;
×
1252
  }
×
1253

1254
  void stop() override
×
1255
  {
1256
    m_running.store(false, std::memory_order_release);
×
1257

1258
    auto& pw = libpipewire_capture::instance();
×
UNCOV
1259
    if(!pw.available)
×
1260
      return;
×
1261

UNCOV
1262
    if(m_loop)
×
1263
      pw.thread_loop_lock(m_loop);
×
1264

1265
    if(m_stream)
×
1266
    {
1267
      pw.stream_disconnect(m_stream);
×
1268
      pw.stream_destroy(m_stream);
×
UNCOV
1269
      m_stream = nullptr;
×
1270
    }
×
1271

1272
    if(m_core)
×
1273
    {
1274
      pw.core_disconnect(m_core);
×
UNCOV
1275
      m_core = nullptr;
×
1276
    }
×
1277

UNCOV
1278
    if(m_loop)
×
1279
      pw.thread_loop_unlock(m_loop);
×
1280

UNCOV
1281
    if(m_loop)
×
1282
      pw.thread_loop_stop(m_loop);
×
1283

1284
    if(m_context)
×
1285
    {
1286
      pw.context_destroy(m_context);
×
UNCOV
1287
      m_context = nullptr;
×
1288
    }
×
1289

1290
    if(m_loop)
×
1291
    {
1292
      pw.thread_loop_destroy(m_loop);
×
UNCOV
1293
      m_loop = nullptr;
×
UNCOV
1294
    }
×
1295

1296
    {
1297
      std::lock_guard lock(m_frameMutex);
×
1298
      m_frameData.clear();
×
1299
      m_frameWidth = 0;
×
1300
      m_frameHeight = 0;
×
1301
      m_frameStride = 0;
×
1302
      m_frameFormat = CapturedFrame::None;
×
1303
      m_dmabufFd = -1;
×
UNCOV
1304
    }
×
1305
  }
×
1306

1307
  CapturedFrame grab() override
×
1308
  {
UNCOV
1309
    if(!m_running.load(std::memory_order_acquire))
×
1310
      return {};
×
1311

1312
    std::lock_guard lock(m_frameMutex);
×
1313

UNCOV
1314
    if(m_frameFormat == CapturedFrame::None)
×
1315
      return {};
×
1316

1317
    CapturedFrame frame;
×
UNCOV
1318
    frame.width = m_frameWidth;
×
1319
    frame.height = m_frameHeight;
×
1320

1321
    if(m_frameFormat == CapturedFrame::DMA_BUF_FD)
×
1322
    {
1323
      frame.type = CapturedFrame::DMA_BUF_FD;
×
1324
      frame.dmabufFd = m_dmabufFd;
×
1325
      frame.drmFormat = m_drmFormat;
×
1326
      frame.drmModifier = m_drmModifier;
×
1327
      frame.dmabufStride = m_dmabufStride;
×
UNCOV
1328
      frame.dmabufOffset = m_dmabufOffset;
×
UNCOV
1329
    }
×
1330
    else
1331
    {
1332
      frame.type = m_frameFormat;
×
UNCOV
1333
      frame.data = m_frameData.data();
×
UNCOV
1334
      frame.stride = m_frameStride;
×
1335
    }
1336

UNCOV
1337
    return frame;
×
UNCOV
1338
  }
×
1339

1340
private:
1341
  // ── PipeWire stream callbacks (static, dispatched via user_data) ──
1342

UNCOV
1343
  static void onStateChanged(
×
1344
      void* data, enum pw_stream_state old_state,
1345
      enum pw_stream_state state, const char* error)
1346
  {
1347
    (void)old_state;
1348
    (void)data;
1349

1350
    static constexpr const char* stateNames[]
1351
        = {"error", "unconnected", "connecting", "paused", "streaming"};
1352
    int idx = static_cast<int>(state) + 1;
×
1353
    if(idx >= 0 && idx < 5)
×
1354
      qDebug() << "WindowCapture PipeWire: stream state:"
×
1355
               << stateNames[idx];
×
1356
    if(error)
×
UNCOV
1357
      qDebug() << "WindowCapture PipeWire: stream error:" << error;
×
1358
  }
×
1359

1360
  static void onParamChanged(void* data, uint32_t id, const void* param)
×
1361
  {
UNCOV
1362
    if(!param || id != SPA_PARAM_Format)
×
1363
      return;
×
1364

UNCOV
1365
    auto* self = static_cast<PipeWireWindowCaptureBackend*>(data);
×
1366

1367
    // Parse the spa_pod_object to extract video format, width, height.
1368
    const auto* pod = static_cast<const spa_pod*>(param);
×
UNCOV
1369
    if(pod->type != SPA_TYPE_OBJECT_Format)
×
1370
      return;
×
1371

1372
    const auto* obj = static_cast<const spa_pod_object*>(param);
×
UNCOV
1373
    const uint8_t* body = reinterpret_cast<const uint8_t*>(&obj->body);
×
UNCOV
1374
    const uint8_t* end = body + pod->size;
×
1375

1376
    // Skip object body header (type 4 + id 4 = 8 bytes)
1377
    const uint8_t* p = body + 8;
×
1378

UNCOV
1379
    while(p + 8 <= end)
×
1380
    {
1381
      uint32_t key, flags;
1382
      std::memcpy(&key, p, 4);
×
1383
      p += 4;
×
UNCOV
1384
      std::memcpy(&flags, p, 4);
×
1385
      p += 4;
×
1386

UNCOV
1387
      if(p + 8 > end)
×
UNCOV
1388
        break;
×
1389

1390
      uint32_t valSize, valType;
1391
      std::memcpy(&valSize, p, 4);
×
UNCOV
1392
      std::memcpy(&valType, p + 4, 4);
×
1393
      const uint8_t* valData = p + 8;
×
1394

UNCOV
1395
      if(key == SPA_FORMAT_VIDEO_format && valType == SPA_TYPE_Id
×
UNCOV
1396
         && valSize >= 4)
×
1397
      {
1398
        uint32_t fmt;
1399
        std::memcpy(&fmt, valData, 4);
×
1400
        self->m_spaVideoFormat = static_cast<spa_video_format>(fmt);
×
UNCOV
1401
        qDebug() << "WindowCapture PipeWire: negotiated format:" << fmt;
×
1402
      }
×
1403
      else if(
UNCOV
1404
          key == SPA_FORMAT_VIDEO_size
×
UNCOV
1405
          && valType == SPA_TYPE_Rectangle && valSize >= 8)
×
1406
      {
1407
        uint32_t w, h;
1408
        std::memcpy(&w, valData, 4);
×
1409
        std::memcpy(&h, valData + 4, 4);
×
1410
        self->m_negotiatedWidth = w;
×
1411
        self->m_negotiatedHeight = h;
×
1412
        qDebug() << "WindowCapture PipeWire: negotiated size:" << w
×
UNCOV
1413
                 << "x" << h;
×
UNCOV
1414
      }
×
1415

1416
      // Advance past the value pod (8-byte header + padded size)
UNCOV
1417
      uint32_t paddedSize = (valSize + 7) & ~7u;
×
1418
      p = p + 8 + paddedSize;
×
1419
    }
1420
  }
×
1421

1422
  static void onProcess(void* data)
×
1423
  {
UNCOV
1424
    auto* self = static_cast<PipeWireWindowCaptureBackend*>(data);
×
1425
    auto& pw = libpipewire_capture::instance();
×
1426

1427
    pw_buffer* buf = pw.stream_dequeue_buffer(self->m_stream);
×
UNCOV
1428
    if(!buf)
×
1429
      return;
×
1430

UNCOV
1431
    spa_buffer* spaBuf = buf->buffer;
×
1432
    if(!spaBuf || spaBuf->n_datas == 0)
×
1433
    {
UNCOV
1434
      pw.stream_queue_buffer(self->m_stream, buf);
×
UNCOV
1435
      return;
×
1436
    }
1437

1438
    spa_data& d = spaBuf->datas[0];
×
UNCOV
1439
    int width = self->m_negotiatedWidth;
×
1440
    int height = self->m_negotiatedHeight;
×
1441

1442
    if(width <= 0 || height <= 0)
×
1443
    {
UNCOV
1444
      pw.stream_queue_buffer(self->m_stream, buf);
×
UNCOV
1445
      return;
×
1446
    }
1447

1448
    if(d.type == SPA_DATA_MemPtr || d.type == SPA_DATA_MemFd)
×
1449
    {
1450
      if(d.data && d.chunk && d.chunk->size > 0)
×
1451
      {
1452
        int stride = d.chunk->stride;
×
UNCOV
1453
        if(stride <= 0)
×
1454
          stride = width * 4;
×
1455

1456
        uint32_t frameOffset = d.chunk->offset;
×
1457
        uint32_t size = d.chunk->size;
×
UNCOV
1458
        const uint8_t* src
×
1459
            = static_cast<const uint8_t*>(d.data) + frameOffset;
×
1460

UNCOV
1461
        CapturedFrame::Type frameType = CapturedFrame::CPU_BGRA;
×
UNCOV
1462
        switch(self->m_spaVideoFormat)
×
1463
        {
1464
          case SPA_VIDEO_FORMAT_RGBx:
1465
          case SPA_VIDEO_FORMAT_RGBA:
UNCOV
1466
            frameType = CapturedFrame::CPU_RGBA;
×
1467
            break;
×
1468
          default:
UNCOV
1469
            frameType = CapturedFrame::CPU_BGRA;
×
UNCOV
1470
            break;
×
1471
        }
1472

1473
        {
1474
          std::lock_guard lock(self->m_frameMutex);
×
1475
          self->m_frameData.resize(size);
×
1476
          std::memcpy(self->m_frameData.data(), src, size);
×
1477
          self->m_frameWidth = width;
×
1478
          self->m_frameHeight = height;
×
1479
          self->m_frameStride = stride;
×
1480
          self->m_frameFormat = frameType;
×
1481
          self->m_dmabufFd = -1;
×
1482
        }
×
1483
      }
×
UNCOV
1484
    }
×
1485
    else if(d.type == SPA_DATA_DmaBuf)
×
1486
    {
1487
      if(d.fd >= 0)
×
1488
      {
1489
        std::lock_guard lock(self->m_frameMutex);
×
1490
        self->m_frameData.clear();
×
1491
        self->m_frameWidth = width;
×
1492
        self->m_frameHeight = height;
×
1493
        self->m_frameStride = 0;
×
1494
        self->m_frameFormat = CapturedFrame::DMA_BUF_FD;
×
1495
        self->m_dmabufFd = d.fd;
×
1496
        self->m_dmabufStride
×
1497
            = d.chunk ? d.chunk->stride : (width * 4);
×
UNCOV
1498
        self->m_dmabufOffset
×
UNCOV
1499
            = d.chunk ? static_cast<int>(d.chunk->offset) : 0;
×
1500

1501
        // Map SPA video format to DRM fourcc
UNCOV
1502
        switch(self->m_spaVideoFormat)
×
1503
        {
1504
          case SPA_VIDEO_FORMAT_BGRx:
UNCOV
1505
            self->m_drmFormat = 0x34325258; // DRM_FORMAT_XRGB8888
×
1506
            break;
×
1507
          case SPA_VIDEO_FORMAT_BGRA:
UNCOV
1508
            self->m_drmFormat = 0x34324152; // DRM_FORMAT_ARGB8888
×
1509
            break;
×
1510
          case SPA_VIDEO_FORMAT_RGBx:
UNCOV
1511
            self->m_drmFormat = 0x34325842; // DRM_FORMAT_XBGR8888
×
1512
            break;
×
1513
          case SPA_VIDEO_FORMAT_RGBA:
UNCOV
1514
            self->m_drmFormat = 0x34324241; // DRM_FORMAT_ABGR8888
×
1515
            break;
×
1516
          default:
UNCOV
1517
            self->m_drmFormat = 0x34325258;
×
1518
            break;
×
1519
        }
1520
        self->m_drmModifier = 0; // LINEAR
×
UNCOV
1521
      }
×
1522
    }
×
1523

UNCOV
1524
    pw.stream_queue_buffer(self->m_stream, buf);
×
UNCOV
1525
  }
×
1526

1527
  // ── Member state ──
1528

1529
  pw_thread_loop* m_loop{};
×
1530
  pw_context* m_context{};
×
1531
  pw_core* m_core{};
×
1532
  pw_stream* m_stream{};
×
1533
  pw_stream_events m_streamEvents{};
×
1534
  spa_hook m_streamListener{};
×
UNCOV
1535
  uint32_t m_pipewireNode{0};
×
UNCOV
1536
  std::atomic<bool> m_running{false};
×
1537

1538
  // Negotiated video format
1539
  spa_video_format m_spaVideoFormat{SPA_VIDEO_FORMAT_UNKNOWN};
×
UNCOV
1540
  int m_negotiatedWidth{0};
×
UNCOV
1541
  int m_negotiatedHeight{0};
×
1542

1543
  // Latest frame (written from PipeWire thread, read from grab())
1544
  std::mutex m_frameMutex;
1545
  std::vector<uint8_t> m_frameData;
1546
  int m_frameWidth{0};
×
1547
  int m_frameHeight{0};
×
UNCOV
1548
  int m_frameStride{0};
×
UNCOV
1549
  CapturedFrame::Type m_frameFormat{CapturedFrame::None};
×
1550

1551
  // DMA-BUF frame info
1552
  int m_dmabufFd{-1};
×
1553
  uint32_t m_drmFormat{0};
×
1554
  uint64_t m_drmModifier{0};
×
UNCOV
1555
  int m_dmabufStride{0};
×
UNCOV
1556
  int m_dmabufOffset{0};
×
1557
};
1558

1559
// ─── Linux factory: X11 first, then PipeWire ────────────────────────────────
1560

UNCOV
1561
std::unique_ptr<WindowCaptureBackend> createWindowCaptureBackend()
×
1562
{
1563
  // Try X11 first (works on X11 sessions and XWayland)
1564
#if defined(HAS_X11_WINDOW_CAPTURE)
UNCOV
1565
  if(auto x11 = createX11Backend())
×
1566
    return x11;
×
1567
#endif
1568

1569
  // Try PipeWire (native Wayland via xdg-desktop-portal)
1570
  auto pw = std::make_unique<PipeWireWindowCaptureBackend>();
×
1571
  if(pw->available())
×
UNCOV
1572
    return pw;
×
1573

UNCOV
1574
  return nullptr;
×
UNCOV
1575
}
×
1576

1577
} // namespace Gfx::WindowCapture
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc