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

ossia / score / 30754311710

02 Aug 2026 03:25PM UTC coverage: 15.222% (-0.04%) from 15.263%
30754311710

Pull #2163

github

web-flow
Merge 8b7bf0aad into e99b6a5da
Pull Request #2163: Fix backwards playback for audio plug-ins (VST, VST3, LV2, JSFX)

0 of 57 new or added lines in 5 files covered. (0.0%)

343 existing lines in 16 files now uncovered.

30381 of 199582 relevant lines covered (15.22%)

1075.26 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