OpenShot Library | libopenshot 1.1.1
WaylandScreenCaptureReader.cpp
Go to the documentation of this file.
1
9// Copyright (c) 2008-2026 OpenShot Studios, LLC
10//
11// SPDX-License-Identifier: LGPL-3.0-or-later
12
13#include "ScreenCaptureReader.h"
14
15#include <algorithm>
16#include <chrono>
17#include <condition_variable>
18#include <cstdint>
19#include <cstring>
20#include <deque>
21#include <mutex>
22#include <sstream>
23#include <stdexcept>
24#include <vector>
25
26#include <gio/gio.h>
27#include <gio/gunixfdlist.h>
28#include <pipewire/pipewire.h>
29#include <spa/buffer/buffer.h>
30#include <spa/buffer/meta.h>
31#include <spa/param/buffers.h>
32#include <spa/pod/builder.h>
33#include <spa/param/video/format-utils.h>
34#include <unistd.h>
35
36#include "Exceptions.h"
37#include "Frame.h"
39#include "Logger.h"
40
41using namespace openshot;
42
43namespace
44{
45 const char* PORTAL_BUS = "org.freedesktop.portal.Desktop";
46 const char* PORTAL_PATH = "/org/freedesktop/portal/desktop";
47 const char* SCREENCAST_IFACE = "org.freedesktop.portal.ScreenCast";
48
49 std::string gerror_message(GError* error)
50 {
51 if (!error) {
52 return "unknown error";
53 }
54 std::string message = error->message ? error->message : "unknown error";
55 g_error_free(error);
56 return message;
57 }
58
59 std::string token_for(const char* prefix)
60 {
61 static uint64_t counter = 0;
62 std::stringstream token;
63 token << prefix << "_" << ++counter;
64 return token.str();
65 }
66
67 struct PortalResponse
68 {
69 bool done = false;
70 bool timed_out = false;
71 uint32_t code = 1;
72 GVariant* results = nullptr;
73 GMainLoop* loop = nullptr;
74
75 ~PortalResponse()
76 {
77 if (results) {
78 g_variant_unref(results);
79 }
80 }
81 };
82
83 void portal_response_callback(
84 GDBusConnection*,
85 const gchar*,
86 const gchar*,
87 const gchar*,
88 const gchar*,
89 GVariant* parameters,
90 gpointer user_data)
91 {
92 auto* response = static_cast<PortalResponse*>(user_data);
93 GVariant* results = nullptr;
94 g_variant_get(parameters, "(u@a{sv})", &response->code, &results);
95 response->results = results;
96 response->done = true;
97 if (response->loop) {
98 g_main_loop_quit(response->loop);
99 }
100 }
101
102 gboolean portal_response_timeout(gpointer user_data)
103 {
104 auto* response = static_cast<PortalResponse*>(user_data);
105 response->timed_out = true;
106 if (response->loop) {
107 g_main_loop_quit(response->loop);
108 }
109 return G_SOURCE_REMOVE;
110 }
111
112 GVariant* wait_for_portal_response(GDBusConnection* connection, const std::string& handle)
113 {
114 PortalResponse response;
115 response.loop = g_main_loop_new(nullptr, FALSE);
116 const guint timeout = g_timeout_add_seconds(60, portal_response_timeout, &response);
117 const guint subscription = g_dbus_connection_signal_subscribe(
118 connection,
119 PORTAL_BUS,
120 "org.freedesktop.portal.Request",
121 "Response",
122 handle.c_str(),
123 nullptr,
124 G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
125 portal_response_callback,
126 &response,
127 nullptr);
128
129 g_main_loop_run(response.loop);
130 g_dbus_connection_signal_unsubscribe(connection, subscription);
131 if (!response.timed_out) {
132 g_source_remove(timeout);
133 }
134 g_main_loop_unref(response.loop);
135 response.loop = nullptr;
136
137 if (response.timed_out) {
138 throw InvalidOptions("Timed out waiting for Wayland screen capture permission.");
139 }
140 if (!response.done || response.code != 0) {
141 throw InvalidOptions("Wayland screen capture permission was denied or cancelled.");
142 }
143 GVariant* results = response.results;
144 response.results = nullptr;
145 return results;
146 }
147
148 std::string call_portal_request(
149 GDBusConnection* connection,
150 const char* method,
151 GVariant* parameters)
152 {
153 GError* error = nullptr;
154 GVariant* result = g_dbus_connection_call_sync(
155 connection,
156 PORTAL_BUS,
157 PORTAL_PATH,
158 SCREENCAST_IFACE,
159 method,
160 parameters,
161 G_VARIANT_TYPE("(o)"),
162 G_DBUS_CALL_FLAGS_NONE,
163 -1,
164 nullptr,
165 &error);
166 if (!result) {
167 throw InvalidOptions("Wayland portal " + std::string(method) + " failed: " + gerror_message(error));
168 }
169
170 const char* handle = nullptr;
171 g_variant_get(result, "(&o)", &handle);
172 std::string handle_path = handle ? handle : "";
173 g_variant_unref(result);
174 return handle_path;
175 }
176
177 struct PortalStreamInfo
178 {
179 uint32_t node_id = 0;
180 uint64_t pipewire_serial = 0;
181 uint32_t source_type = 0;
182 int width = 0;
183 int height = 0;
184 };
185
186 PortalStreamInfo stream_info_from_results(GVariant* results)
187 {
188 GVariant* streams = g_variant_lookup_value(results, "streams", G_VARIANT_TYPE("a(ua{sv})"));
189 if (!streams || g_variant_n_children(streams) == 0) {
190 if (streams) {
191 g_variant_unref(streams);
192 }
193 throw InvalidOptions("Wayland portal did not return a PipeWire stream.");
194 }
195
196 PortalStreamInfo stream_info;
197 GVariant* properties = nullptr;
198 GVariant* child = g_variant_get_child_value(streams, 0);
199 g_variant_get(child, "(u@a{sv})", &stream_info.node_id, &properties);
200 if (properties) {
201 uint64_t serial = 0;
202 uint32_t source_type = 0;
203 int width = 0;
204 int height = 0;
205 if (g_variant_lookup(properties, "pipewire-serial", "t", &serial)) {
206 stream_info.pipewire_serial = serial;
207 }
208 if (g_variant_lookup(properties, "source_type", "u", &source_type)) {
209 stream_info.source_type = source_type;
210 }
211 if (g_variant_lookup(properties, "size", "(ii)", &width, &height)) {
212 stream_info.width = width;
213 stream_info.height = height;
214 }
215 g_variant_unref(properties);
216 }
217 g_variant_unref(child);
218 g_variant_unref(streams);
219 return stream_info;
220 }
221
222 int open_pipewire_remote(GDBusConnection* connection, const std::string& session_handle)
223 {
224 GVariantBuilder options;
225 g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
226
227 GError* error = nullptr;
228 GUnixFDList* out_fds = nullptr;
229 GVariant* result = g_dbus_connection_call_with_unix_fd_list_sync(
230 connection,
231 PORTAL_BUS,
232 PORTAL_PATH,
233 SCREENCAST_IFACE,
234 "OpenPipeWireRemote",
235 g_variant_new("(oa{sv})", session_handle.c_str(), &options),
236 G_VARIANT_TYPE("(h)"),
237 G_DBUS_CALL_FLAGS_NONE,
238 -1,
239 nullptr,
240 &out_fds,
241 nullptr,
242 &error);
243 if (!result) {
244 throw InvalidOptions("Wayland portal OpenPipeWireRemote failed: " + gerror_message(error));
245 }
246
247 int fd_index = -1;
248 g_variant_get(result, "(h)", &fd_index);
249 g_variant_unref(result);
250 int fd = g_unix_fd_list_get(out_fds, fd_index, &error);
251 g_object_unref(out_fds);
252 if (fd < 0) {
253 throw InvalidOptions("Unable to obtain PipeWire remote file descriptor: " + gerror_message(error));
254 }
255 return fd;
256 }
257
258 struct CapturedFrame
259 {
260 int width = 0;
261 int height = 0;
262 std::vector<unsigned char> rgba;
263 };
264}
265
267{
268public:
270 : settings(new_settings)
271 , info(new_info)
272 , connection(nullptr)
273 , thread_loop(nullptr)
274 , context(nullptr)
275 , core(nullptr)
276 , stream(nullptr)
277 , session_closed_subscription(0)
278 , frames_read(0)
279 , dropped_packets(0)
280 , open(false)
281 , streaming(false)
282 , stream_error(false)
283 , crop_logged(false)
284 , have_last_header_sequence(false)
285 , last_header_sequence(0)
286 , header_sequence_ordering_active(false)
287 , have_last_header_pts(false)
288 , last_header_pts(0)
289 , header_drop_log_count(0)
290 {
291 }
292
294 {
295 Close();
296 }
297
298 void Open() override
299 {
300 if (open) {
301 return;
302 }
303
304 GError* error = nullptr;
305 connection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error);
306 if (!connection) {
307 throw InvalidOptions("Unable to connect to the session bus for Wayland capture: " + gerror_message(error));
308 }
309
310 session_handle = CreatePortalSession();
311 SubscribePortalSessionClosed();
312 SelectPortalSources(session_handle);
313 stream_info = StartPortalSession(session_handle);
314 ApplyPortalStreamInfo();
315 Logger::Instance()->Log(
316 "Wayland portal stream selected: node_id=" + std::to_string(stream_info.node_id) +
317 " pipewire_serial=" + std::to_string(stream_info.pipewire_serial) +
318 " source_type=" + std::to_string(stream_info.source_type) +
319 " portal_size=" + std::to_string(stream_info.width) + "x" + std::to_string(stream_info.height));
320 const int pipewire_fd = open_pipewire_remote(connection, session_handle);
321 OpenPipeWireStream(pipewire_fd);
322 open = true;
323 }
324
325 void Close() override
326 {
327 open = false;
328 streaming = false;
329
330 if (thread_loop) {
331 pw_thread_loop_stop(thread_loop);
332 }
333 if (connection && session_closed_subscription) {
334 g_dbus_connection_signal_unsubscribe(connection, session_closed_subscription);
335 session_closed_subscription = 0;
336 }
337 if (connection && !session_handle.empty()) {
338 GError* error = nullptr;
339 GVariant* result = g_dbus_connection_call_sync(
340 connection,
341 PORTAL_BUS,
342 session_handle.c_str(),
343 "org.freedesktop.portal.Session",
344 "Close",
345 nullptr,
346 nullptr,
347 G_DBUS_CALL_FLAGS_NONE,
348 -1,
349 nullptr,
350 &error);
351 if (result) {
352 g_variant_unref(result);
353 } else if (error) {
354 g_error_free(error);
355 }
356 session_handle.clear();
357 }
358 if (stream) {
359 pw_stream_destroy(stream);
360 stream = nullptr;
361 }
362 if (core) {
363 pw_core_disconnect(core);
364 core = nullptr;
365 }
366 if (context) {
367 pw_context_destroy(context);
368 context = nullptr;
369 }
370 if (thread_loop) {
371 pw_thread_loop_destroy(thread_loop);
372 thread_loop = nullptr;
373 }
374 if (connection) {
375 g_object_unref(connection);
376 connection = nullptr;
377 }
378 }
379
380 bool IsOpen() const override
381 {
382 return open;
383 }
384
386 {
387 CaptureReaderStats stats;
388 stats.is_open = open;
389 stats.frames_read = frames_read;
390 stats.dropped_packets = dropped_packets;
391 const double fps = settings.fps.den != 0 ? static_cast<double>(settings.fps.num) / static_cast<double>(settings.fps.den) : 0.0;
392 stats.duration = fps > 0.0 ? static_cast<double>(frames_read) / fps : 0.0;
393 return stats;
394 }
395
396 std::shared_ptr<Frame> GetFrame(int64_t number) override
397 {
398 CapturedFrame captured;
399 {
400 std::unique_lock<std::mutex> lock(queue_mutex);
401 const auto wait_duration = std::chrono::milliseconds(
403 settings.fps.num,
404 settings.fps.den,
405 have_last_frame));
406 const auto deadline = std::chrono::steady_clock::now() + wait_duration;
407 while (frame_queue.empty() && !stream_error && open && std::chrono::steady_clock::now() < deadline) {
408 lock.unlock();
409 while (g_main_context_iteration(nullptr, FALSE)) {
410 }
411 lock.lock();
412 queue_condition.wait_for(lock, std::chrono::milliseconds(100));
413 }
414 if (frame_queue.empty() && !stream_error && open) {
415 if (have_last_frame) {
416 captured = last_frame;
417 } else {
418 throw InvalidFile("Timed out waiting for the first Wayland capture frame.", "wayland");
419 }
420 }
421 if (stream_error) {
422 throw InvalidFile("Wayland capture stream failed.", "wayland");
423 }
424 if (frame_queue.empty()) {
425 if (!open) {
426 throw ReaderClosed("The Wayland screen capture stream is closed.");
427 }
428 if (!have_last_frame) {
429 throw InvalidFile("Timed out waiting for the first Wayland capture frame.", "wayland");
430 }
431 } else {
432 captured = std::move(frame_queue.front());
433 frame_queue.pop_front();
434 last_frame = captured;
435 have_last_frame = true;
436 }
437 }
438
439 const int bytes_per_pixel = 4;
440 const size_t buffer_size = static_cast<size_t>(captured.width) * captured.height * bytes_per_pixel;
441 unsigned char* buffer = static_cast<unsigned char*>(malloc(buffer_size));
442 if (!buffer) {
443 throw OutOfMemory("Unable to allocate Wayland capture frame buffer.", "wayland");
444 }
445 std::memcpy(buffer, captured.rgba.data(), buffer_size);
446
447 auto frame = std::make_shared<Frame>(number, captured.width, captured.height, "#000000");
448 frame->AddImage(captured.width, captured.height, bytes_per_pixel, QImage::Format_RGBA8888, buffer);
449 frames_read++;
450 return frame;
451 }
452
453private:
454 std::string CreatePortalSession()
455 {
456 GVariantBuilder options;
457 g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
458 g_variant_builder_add(&options, "{sv}", "handle_token", g_variant_new_string(token_for("openshot_create").c_str()));
459 g_variant_builder_add(&options, "{sv}", "session_handle_token", g_variant_new_string(token_for("openshot_session").c_str()));
460
461 const std::string handle = call_portal_request(connection, "CreateSession", g_variant_new("(a{sv})", &options));
462 GVariant* results = wait_for_portal_response(connection, handle);
463
464 const char* session = nullptr;
465 if (!g_variant_lookup(results, "session_handle", "&s", &session) || !session) {
466 g_variant_unref(results);
467 throw InvalidOptions("Wayland portal did not return a screencast session handle.");
468 }
469 std::string session_handle = session;
470 g_variant_unref(results);
471 return session_handle;
472 }
473
474 void SubscribePortalSessionClosed()
475 {
476 if (!connection || session_handle.empty()) {
477 return;
478 }
479 session_closed_subscription = g_dbus_connection_signal_subscribe(
480 connection,
481 PORTAL_BUS,
482 "org.freedesktop.portal.Session",
483 "Closed",
484 session_handle.c_str(),
485 nullptr,
486 G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
487 OnPortalSessionClosed,
488 this,
489 nullptr);
490 }
491
492 void SelectPortalSources(const std::string& session_handle)
493 {
494 const uint32_t source_monitor = 1;
495 const uint32_t source_window = 2;
496 const uint32_t cursor_hidden = 1;
497 const uint32_t cursor_embedded = 2;
498
499 GVariantBuilder options;
500 g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
501 g_variant_builder_add(&options, "{sv}", "handle_token", g_variant_new_string(token_for("openshot_select").c_str()));
502 g_variant_builder_add(&options, "{sv}", "types", g_variant_new_uint32(source_monitor | source_window));
503 g_variant_builder_add(&options, "{sv}", "multiple", g_variant_new_boolean(FALSE));
504 g_variant_builder_add(&options, "{sv}", "cursor_mode", g_variant_new_uint32(settings.include_cursor ? cursor_embedded : cursor_hidden));
505
506 const std::string handle = call_portal_request(
507 connection,
508 "SelectSources",
509 g_variant_new("(oa{sv})", session_handle.c_str(), &options));
510 GVariant* results = wait_for_portal_response(connection, handle);
511 g_variant_unref(results);
512 }
513
514 PortalStreamInfo StartPortalSession(const std::string& session_handle)
515 {
516 GVariantBuilder options;
517 g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
518 g_variant_builder_add(&options, "{sv}", "handle_token", g_variant_new_string(token_for("openshot_start").c_str()));
519
520 const std::string handle = call_portal_request(
521 connection,
522 "Start",
523 g_variant_new("(osa{sv})", session_handle.c_str(), "", &options));
524 GVariant* results = wait_for_portal_response(connection, handle);
525 const PortalStreamInfo result_stream_info = stream_info_from_results(results);
526 g_variant_unref(results);
527 return result_stream_info;
528 }
529
530 void ApplyPortalStreamInfo()
531 {
532 if (stream_info.width <= 0 || stream_info.height <= 0) {
533 return;
534 }
535 info.width = stream_info.width;
536 info.height = stream_info.height;
537 info.display_ratio = Fraction(stream_info.width, stream_info.height);
538 info.display_ratio.Reduce();
539 }
540
541 void OpenPipeWireStream(int pipewire_fd)
542 {
543 pw_init(nullptr, nullptr);
544
545 thread_loop = pw_thread_loop_new("openshot-wayland-capture", nullptr);
546 if (!thread_loop) {
547 throw InvalidOptions("Unable to create PipeWire thread loop.");
548 }
549 context = pw_context_new(pw_thread_loop_get_loop(thread_loop), nullptr, 0);
550 if (!context) {
551 throw InvalidOptions("Unable to create PipeWire context.");
552 }
553 core = pw_context_connect_fd(context, pipewire_fd, nullptr, 0);
554 if (!core) {
555 close(pipewire_fd);
556 throw InvalidOptions("Unable to connect to the portal PipeWire remote.");
557 }
558
559 pw_properties* props = pw_properties_new(
560 PW_KEY_MEDIA_TYPE, "Video",
561 PW_KEY_MEDIA_CATEGORY, "Capture",
562 PW_KEY_MEDIA_ROLE, "Screen",
563 nullptr);
564 if (stream_info.pipewire_serial > 0) {
565 pw_properties_set(props, PW_KEY_TARGET_OBJECT, std::to_string(stream_info.pipewire_serial).c_str());
566 }
567 stream = pw_stream_new(core, "OpenShot Wayland Screen Capture", props);
568 if (!stream) {
569 throw InvalidOptions("Unable to create PipeWire capture stream.");
570 }
571
572 pw_stream_add_listener(stream, &stream_listener, &stream_events, this);
573
574 if (pw_thread_loop_start(thread_loop) < 0) {
575 throw InvalidOptions("Unable to start PipeWire capture thread loop.");
576 }
577
578 uint8_t format_buffer[1024];
579 spa_pod_builder builder = SPA_POD_BUILDER_INIT(format_buffer, sizeof(format_buffer));
580 const spa_fraction requested_framerate = SPA_FRACTION(
581 static_cast<uint32_t>(std::max(1, settings.fps.num)),
582 static_cast<uint32_t>(std::max(1, settings.fps.den)));
583 const spa_fraction variable_framerate = SPA_FRACTION(0, 1);
584 const spa_pod* params[1];
585 params[0] = static_cast<const spa_pod*>(spa_pod_builder_add_object(
586 &builder,
587 SPA_TYPE_OBJECT_Format,
588 SPA_PARAM_EnumFormat,
589 SPA_FORMAT_mediaType,
590 SPA_POD_Id(SPA_MEDIA_TYPE_video),
591 SPA_FORMAT_mediaSubtype,
592 SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
593 SPA_FORMAT_VIDEO_format,
594 SPA_POD_CHOICE_ENUM_Id(
595 4,
596 SPA_VIDEO_FORMAT_BGRx,
597 SPA_VIDEO_FORMAT_RGBA,
598 SPA_VIDEO_FORMAT_BGRA,
599 SPA_VIDEO_FORMAT_RGBx),
600 SPA_FORMAT_VIDEO_framerate,
601 SPA_POD_Fraction(&variable_framerate),
602 SPA_FORMAT_VIDEO_maxFramerate,
603 SPA_POD_Fraction(&requested_framerate)));
604
605 pw_thread_loop_lock(thread_loop);
606 const uint32_t target_id = stream_info.pipewire_serial > 0 ? PW_ID_ANY : stream_info.node_id;
607 const int result = pw_stream_connect(
608 stream,
609 PW_DIRECTION_INPUT,
610 target_id,
611 static_cast<pw_stream_flags>(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS),
612 params,
613 1);
614 if (result < 0) {
615 pw_thread_loop_unlock(thread_loop);
616 throw InvalidOptions("Unable to connect to PipeWire capture stream.");
617 }
618
619 while (!streaming && !stream_error) {
620 if (pw_thread_loop_timed_wait(thread_loop, 10) < 0) {
621 stream_error = true;
622 break;
623 }
624 }
625 pw_thread_loop_unlock(thread_loop);
626
627 if (stream_error) {
628 throw InvalidOptions("PipeWire capture stream failed to start or timed out.");
629 }
630 }
631
632 static void OnPortalSessionClosed(
633 GDBusConnection*,
634 const gchar*,
635 const gchar*,
636 const gchar*,
637 const gchar*,
638 GVariant*,
639 gpointer user_data)
640 {
641 auto* self = static_cast<WaylandScreenCaptureReader*>(user_data);
642 self->open = false;
643 self->stream_error = true;
644 if (self->thread_loop) {
645 pw_thread_loop_signal(self->thread_loop, false);
646 }
647 self->queue_condition.notify_all();
648 }
649
650 static void OnStreamStateChanged(void* data, pw_stream_state, pw_stream_state state, const char*)
651 {
652 auto* self = static_cast<WaylandScreenCaptureReader*>(data);
653 if (state == PW_STREAM_STATE_STREAMING) {
654 self->streaming = true;
655 pw_thread_loop_signal(self->thread_loop, false);
656 } else if (state == PW_STREAM_STATE_ERROR || state == PW_STREAM_STATE_UNCONNECTED) {
657 self->stream_error = true;
658 pw_thread_loop_signal(self->thread_loop, false);
659 self->queue_condition.notify_all();
660 }
661 }
662
663 static void OnStreamParamChanged(void* data, uint32_t id, const spa_pod* param)
664 {
665 if (id != SPA_PARAM_Format || !param) {
666 return;
667 }
668 auto* self = static_cast<WaylandScreenCaptureReader*>(data);
669 spa_video_info info = {};
670 if (spa_format_parse(param, &info.media_type, &info.media_subtype) < 0 ||
671 info.media_type != SPA_MEDIA_TYPE_video ||
672 info.media_subtype != SPA_MEDIA_SUBTYPE_raw ||
673 spa_format_video_raw_parse(param, &info.info.raw) < 0) {
674 return;
675 }
676
677 self->video_format = info.info.raw.format;
678 self->stream_width = static_cast<int>(info.info.raw.size.width);
679 self->stream_height = static_cast<int>(info.info.raw.size.height);
680 if (info.info.raw.framerate.num > 0 && info.info.raw.framerate.denom > 0) {
681 self->info.fps = Fraction(
682 static_cast<int>(info.info.raw.framerate.num),
683 static_cast<int>(info.info.raw.framerate.denom));
684 self->info.video_timebase = self->info.fps.Reciprocal();
685 }
686 if (self->stream_width > 0 && self->stream_height > 0) {
687 Logger::Instance()->Log(
688 "Wayland PipeWire stream format: " +
689 std::to_string(self->stream_width) + "x" + std::to_string(self->stream_height) +
690 " format=" + std::to_string(self->video_format) +
691 " framerate=" + std::to_string(info.info.raw.framerate.num) +
692 "/" + std::to_string(info.info.raw.framerate.denom) +
693 " max_framerate=" + std::to_string(info.info.raw.max_framerate.num) +
694 "/" + std::to_string(info.info.raw.max_framerate.denom));
695 self->info.width = self->stream_width;
696 self->info.height = self->stream_height;
697 self->info.display_ratio = Fraction(self->stream_width, self->stream_height);
698 self->info.display_ratio.Reduce();
699 }
700
701 uint8_t params_buffer[1024];
702 spa_pod_builder builder = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer));
703 const int stride = self->stream_width * 4;
704 const int buffer_size = stride * self->stream_height;
705 const spa_pod* params[3];
706 params[0] = static_cast<const spa_pod*>(spa_pod_builder_add_object(
707 &builder,
708 SPA_TYPE_OBJECT_ParamBuffers,
709 SPA_PARAM_Buffers,
710 SPA_PARAM_BUFFERS_buffers,
711 SPA_POD_CHOICE_RANGE_Int(8, 2, 32),
712 SPA_PARAM_BUFFERS_blocks,
713 SPA_POD_Int(1),
714 SPA_PARAM_BUFFERS_size,
715 SPA_POD_Int(buffer_size),
716 SPA_PARAM_BUFFERS_stride,
717 SPA_POD_Int(stride),
718 SPA_PARAM_BUFFERS_dataType,
719 SPA_POD_CHOICE_FLAGS_Int(
720 (1 << SPA_DATA_MemPtr) | (1 << SPA_DATA_MemFd))));
721 params[1] = static_cast<const spa_pod*>(spa_pod_builder_add_object(
722 &builder,
723 SPA_TYPE_OBJECT_ParamMeta,
724 SPA_PARAM_Meta,
725 SPA_PARAM_META_type,
726 SPA_POD_Id(SPA_META_Header),
727 SPA_PARAM_META_size,
728 SPA_POD_Int(sizeof(spa_meta_header))));
729 params[2] = static_cast<const spa_pod*>(spa_pod_builder_add_object(
730 &builder,
731 SPA_TYPE_OBJECT_ParamMeta,
732 SPA_PARAM_Meta,
733 SPA_PARAM_META_type,
734 SPA_POD_Id(SPA_META_VideoCrop),
735 SPA_PARAM_META_size,
736 SPA_POD_Int(sizeof(spa_meta_region))));
737 pw_stream_update_params(self->stream, params, 3);
738 }
739
740 static void OnStreamProcess(void* data)
741 {
742 auto* self = static_cast<WaylandScreenCaptureReader*>(data);
743 // Drain the stream and process only the newest buffer. Returning stale
744 // buffers immediately prevents a damage-driven window stream from
745 // accumulating latency after it becomes visible again.
746 pw_buffer* buffer = nullptr;
747 pw_buffer* next_buffer = pw_stream_dequeue_buffer(self->stream);
748 while (next_buffer) {
749 if (buffer) {
750 pw_stream_queue_buffer(self->stream, buffer);
751 self->dropped_packets++;
752 }
753 buffer = next_buffer;
754 next_buffer = pw_stream_dequeue_buffer(self->stream);
755 }
756 if (!buffer) {
757 return;
758 }
759
760 self->CopyPipeWireBuffer(buffer);
761 pw_stream_queue_buffer(self->stream, buffer);
762 }
763
764 void CopyPipeWireBuffer(pw_buffer* buffer)
765 {
766 spa_buffer* spa_buffer = buffer->buffer;
767 if (!spa_buffer || spa_buffer->n_datas == 0 || !spa_buffer->datas[0].data || stream_width <= 0 || stream_height <= 0) {
768 dropped_packets++;
769 return;
770 }
771
772 auto* header = static_cast<spa_meta_header*>(
773 spa_buffer_find_meta_data(spa_buffer, SPA_META_Header, sizeof(spa_meta_header)));
774 if (header && !AcceptHeader(*header)) {
775 dropped_packets++;
776 return;
777 }
778
779 spa_data& data = spa_buffer->datas[0];
780 const spa_chunk* chunk = data.chunk;
781 int crop_x = 0;
782 int crop_y = 0;
783 int crop_width = stream_width;
784 int crop_height = stream_height;
785 auto* crop = static_cast<spa_meta_region*>(
786 spa_buffer_find_meta_data(spa_buffer, SPA_META_VideoCrop, sizeof(spa_meta_region)));
787 if (crop && spa_meta_region_is_valid(crop)) {
788 crop_x = std::max(0, crop->region.position.x);
789 crop_y = std::max(0, crop->region.position.y);
790 crop_width = std::min(static_cast<int>(crop->region.size.width), stream_width - crop_x);
791 crop_height = std::min(static_cast<int>(crop->region.size.height), stream_height - crop_y);
792 if (!crop_logged) {
793 Logger::Instance()->Log(
794 "Wayland PipeWire video crop: x=" + std::to_string(crop_x) +
795 " y=" + std::to_string(crop_y) +
796 " width=" + std::to_string(crop_width) +
797 " height=" + std::to_string(crop_height));
798 crop_logged = true;
799 }
800 }
801
802 const auto layout = wayland::ResolvePackedVideoLayout(
803 static_cast<size_t>(data.maxsize),
804 chunk ? static_cast<size_t>(chunk->offset) : 0,
805 chunk ? static_cast<size_t>(chunk->size) : static_cast<size_t>(data.maxsize),
806 chunk ? chunk->stride : 0,
807 stream_width,
808 stream_height,
809 crop_x,
810 crop_y,
811 crop_width,
812 crop_height);
813 if (!layout.valid) {
814 dropped_packets++;
815 return;
816 }
817
818 CapturedFrame frame;
819 frame.width = layout.width;
820 frame.height = layout.height;
821 frame.rgba.assign(static_cast<size_t>(frame.width) * frame.height * 4, 0);
822
823 std::vector<uint8_t> source_row(static_cast<size_t>(frame.width) * 4);
824 for (int y = 0; y < frame.height; ++y) {
825 const size_t logical_offset =
826 static_cast<size_t>(y + layout.crop_y) * layout.stride
827 + static_cast<size_t>(layout.crop_x) * 4;
829 static_cast<const uint8_t*>(data.data),
830 static_cast<size_t>(data.maxsize),
831 layout.offset,
832 logical_offset,
833 source_row.data(),
834 source_row.size())) {
835 dropped_packets++;
836 return;
837 }
838 for (int x = 0; x < frame.width; ++x) {
839 const uint8_t* pixel = source_row.data() + static_cast<size_t>(x) * 4;
840 uint8_t r = 0;
841 uint8_t g = 0;
842 uint8_t b = 0;
843 uint8_t a = 255;
844 switch (video_format) {
845 case SPA_VIDEO_FORMAT_RGBA:
846 r = pixel[0]; g = pixel[1]; b = pixel[2]; a = pixel[3];
847 break;
848 case SPA_VIDEO_FORMAT_RGBx:
849 r = pixel[0]; g = pixel[1]; b = pixel[2];
850 break;
851 case SPA_VIDEO_FORMAT_BGRA:
852 b = pixel[0]; g = pixel[1]; r = pixel[2]; a = pixel[3];
853 break;
854 case SPA_VIDEO_FORMAT_BGRx:
855 default:
856 b = pixel[0]; g = pixel[1]; r = pixel[2];
857 break;
858 }
859 const size_t dst = (static_cast<size_t>(y) * crop_width + x) * 4;
860 frame.rgba[dst + 0] = r;
861 frame.rgba[dst + 1] = g;
862 frame.rgba[dst + 2] = b;
863 frame.rgba[dst + 3] = a;
864 }
865 }
866
867 {
868 std::lock_guard<std::mutex> lock(queue_mutex);
869 if (frame_queue.size() > 3) {
870 frame_queue.pop_front();
871 dropped_packets++;
872 }
873 frame_queue.push_back(std::move(frame));
874 }
875 if (info.width != frame.width || info.height != frame.height) {
876 info.width = frame.width;
877 info.height = frame.height;
878 info.display_ratio = Fraction(frame.width, frame.height);
879 info.display_ratio.Reduce();
880 }
881 queue_condition.notify_one();
882 }
883
884 bool AcceptHeader(const spa_meta_header& header)
885 {
886 if (header.flags & (SPA_META_HEADER_FLAG_CORRUPTED | SPA_META_HEADER_FLAG_GAP)) {
887 LogDroppedHeader("flags", header);
888 return false;
889 }
890
891 const bool discontinuity = (header.flags & SPA_META_HEADER_FLAG_DISCONT) != 0;
892 if (!discontinuity && have_last_header_sequence && header.seq > last_header_sequence) {
893 header_sequence_ordering_active = true;
894 }
895 if (!discontinuity && header_sequence_ordering_active && header.seq <= last_header_sequence) {
896 LogDroppedHeader("sequence", header);
897 return false;
898 }
899 if (!discontinuity && header.pts >= 0 && have_last_header_pts && header.pts < last_header_pts) {
900 LogDroppedHeader("pts", header);
901 return false;
902 }
903
904 have_last_header_sequence = true;
905 last_header_sequence = header.seq;
906 if (header.pts >= 0) {
907 have_last_header_pts = true;
908 last_header_pts = header.pts;
909 }
910 return true;
911 }
912
913 void LogDroppedHeader(const std::string& reason, const spa_meta_header& header)
914 {
915 if (header_drop_log_count >= 5) {
916 return;
917 }
918 Logger::Instance()->Log(
919 "Wayland PipeWire dropped non-monotonic frame: reason=" + reason +
920 " flags=" + std::to_string(header.flags) +
921 " seq=" + std::to_string(header.seq) +
922 " last_seq=" + (have_last_header_sequence ? std::to_string(last_header_sequence) : std::string("none")) +
923 " seq_active=" + std::to_string(header_sequence_ordering_active ? 1 : 0) +
924 " pts=" + std::to_string(header.pts) +
925 " last_pts=" + (have_last_header_pts ? std::to_string(last_header_pts) : std::string("none")));
926 header_drop_log_count++;
927 }
928
929 ScreenCaptureSettings settings;
930 ReaderInfo& info;
931 GDBusConnection* connection;
932 pw_thread_loop* thread_loop;
933 pw_context* context;
934 pw_core* core;
935 pw_stream* stream;
936 spa_hook stream_listener;
937 PortalStreamInfo stream_info;
938 std::string session_handle;
939 guint session_closed_subscription;
940 int stream_width = 0;
941 int stream_height = 0;
942 spa_video_format video_format = SPA_VIDEO_FORMAT_BGRx;
943 int64_t frames_read;
944 int dropped_packets;
945 bool open;
946 bool streaming;
947 bool stream_error;
948 bool crop_logged;
949 bool have_last_header_sequence;
950 uint64_t last_header_sequence;
951 bool header_sequence_ordering_active;
952 bool have_last_header_pts;
953 int64_t last_header_pts;
954 int header_drop_log_count;
955 std::mutex queue_mutex;
956 std::condition_variable queue_condition;
957 std::deque<CapturedFrame> frame_queue;
958 CapturedFrame last_frame;
959 bool have_last_frame = false;
960
961 static const pw_stream_events stream_events;
962};
963
964const pw_stream_events WaylandScreenCaptureReader::stream_events = {
965 PW_VERSION_STREAM_EVENTS,
966 nullptr,
967 WaylandScreenCaptureReader::OnStreamStateChanged,
968 nullptr,
969 nullptr,
970 WaylandScreenCaptureReader::OnStreamParamChanged,
971 nullptr,
972 nullptr,
973 WaylandScreenCaptureReader::OnStreamProcess,
974 nullptr,
975 nullptr,
976 nullptr
977};
978
979extern "C" std::unique_ptr<ScreenCaptureReader::CaptureBackendReader> OpenShotCreateWaylandScreenCaptureReader(
980 const ScreenCaptureSettings& settings,
981 ReaderInfo& info)
982{
983 return std::make_unique<WaylandScreenCaptureReader>(settings, info);
984}
Header file for all Exception classes.
Header file for Frame class.
Header file for live screen capture readers.
Bounds-safe helpers for mapped PipeWire video buffers.
std::unique_ptr< ScreenCaptureReader::CaptureBackendReader > OpenShotCreateWaylandScreenCaptureReader(const ScreenCaptureSettings &settings, ReaderInfo &info)
WaylandScreenCaptureReader(const ScreenCaptureSettings &new_settings, ReaderInfo &new_info)
CaptureReaderStats GetStats() const override
std::shared_ptr< Frame > GetFrame(int64_t number) override
This class represents a fraction.
Definition: Fraction.h:30
int num
Numerator for the fraction.
Definition: Fraction.h:32
void Reduce()
Reduce this fraction (i.e. 640/480 = 4/3)
Definition: Fraction.cpp:65
int den
Denominator for the fraction.
Definition: Fraction.h:33
Exception for files that can not be found or opened.
Definition: Exceptions.h:194
Exception when invalid encoding options are used.
Definition: Exceptions.h:239
Exception when memory could not be allocated.
Definition: Exceptions.h:355
Exception when a reader is closed, and a frame is requested.
Definition: Exceptions.h:370
bool CopyWrappedBytes(const uint8_t *source, size_t max_size, size_t chunk_offset, size_t logical_offset, uint8_t *destination, size_t byte_count)
PackedVideoLayout ResolvePackedVideoLayout(size_t max_size, size_t chunk_offset, size_t chunk_size, int chunk_stride, int stream_width, int stream_height, int crop_x, int crop_y, int crop_width, int crop_height)
int DamageFrameWaitMilliseconds(int fps_num, int fps_den, bool have_last_frame)
This namespace is the default namespace for all code in the openshot library.
Definition: AnimatedCurve.h:24
This struct contains info about a media file, such as height, width, frames per second,...
Definition: ReaderBase.h:39
int width
The width of the video (in pixesl)
Definition: ReaderBase.h:46
openshot::Fraction fps
Frames per second, as a fraction (i.e. 24/1 = 24 fps)
Definition: ReaderBase.h:48
openshot::Fraction display_ratio
The ratio of width to height of the video stream (i.e. 640x480 has a ratio of 4/3)
Definition: ReaderBase.h:51
int height
The height of the video (in pixels)
Definition: ReaderBase.h:45