OpenShot Library | libopenshot  1.0.0
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"
38 #include "WaylandBufferUtilities.h"
39 #include "ZmqLogger.h"
40 
41 using namespace openshot;
42 
43 namespace
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 {
268 public:
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();
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 
385  CaptureReaderStats GetStats() const override
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 
453 private:
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) {
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) {
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  }
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 
964 const 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 
979 extern "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)
Header file for ZeroMQ-based Logger class.
std::shared_ptr< Frame > GetFrame(int64_t number) override
WaylandScreenCaptureReader(const ScreenCaptureSettings &new_settings, ReaderInfo &new_info)
CaptureReaderStats GetStats() const override
This class represents a fraction.
Definition: Fraction.h:30
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
void Log(std::string message)
Log message to all subscribers of this logger (if any)
Definition: ZmqLogger.cpp:103
static ZmqLogger * Instance()
Create or get an instance of this logger singleton (invoke the class with this method)
Definition: ZmqLogger.cpp:35
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