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