1#define _GNU_SOURCE
2#include <errno.h>
3#include <fcntl.h>
4#include <signal.h>
5#include <stdlib.h>
6#include <string.h>
7#include <sys/file.h>
8#include <sys/ipc.h>
9#include <sys/shm.h>
10#include <unistd.h>
11#include <gio/gunixfdlist.h>
12#include <glib-unix.h>
13#include <X11/Xlib.h>
14#include <X11/Xutil.h>
15#include <X11/extensions/Xfixes.h>
16#include <X11/extensions/XShm.h>
17#include <pipewire/pipewire.h>
18#include <spa/param/video/format-utils.h>
19#include <spa/buffer/meta.h>
20#include "cursor.h"
21
22static GMainLoop *main_loop;
23static int channel = -1, capturing, initial = 1, stopping, suspended;
24static guint read_watch, write_watch;
25static GByteArray *out, *cursor_packet;
26static size_t sent;
27static gint64 queued_at;
28static uint8_t request[16];
29static size_t received, request_size = 12;
30static uint8_t *before;
31static uint32_t width, height, cursor_id, pointer[SG_POINTER_WORDS];
32static const char *channel_path;
33static uint32_t stats[SG_CAPTURE_STATS_WORDS];
34static gint64 stats_at;
35static Display *display;
36static Window root;
37static XImage *ximage;
38static XShmSegmentInfo shared = {.shmid = -1};
39static int shm_attached, x_error, cursor_event, cursor_changed = 1;
40static unsigned long cursor_serial;
41static gint64 next_capture;
42static GDBusConnection *bus, *login_bus;
43static char *session_path, *request_path;
44static int portal_step, session_active = 1, session_locked, restart;
45static struct pw_main_loop *pw_loop;
46static struct pw_context *pw_context;
47static struct pw_core *pw_core;
48static struct pw_stream *pw_stream;
49static struct spa_hook stream_listener;
50static struct spa_video_info_raw video_info;
51static char *restore_path;
52static guint portal_subscription, session_subscription, pw_watch;
53
54static void close_channel(void) {
55 if (read_watch) { g_source_remove(read_watch); read_watch = 0; }
56 if (write_watch) { g_source_remove(write_watch); write_watch = 0; }
57 if (channel >= 0) close(channel);
58 channel = -1; capturing = 0; stopping = 0; initial = 1;
59 received = 0; request_size = 12; sent = 0;
60 g_byte_array_set_size(out, 0);
61 if (pw_stream) pw_stream_set_active(pw_stream, false);
62}
63
64static gboolean flush_output(gint fd, GIOCondition condition, gpointer unused) {
65 (void)fd; (void)unused;
66 write_watch = 0;
67 if (condition & (G_IO_HUP | G_IO_ERR)) { close_channel(); return G_SOURCE_REMOVE; }
68 while (sent < out->len) {
69 size_t amount = out->len - sent;
70 if (amount > 65536) amount = 65536;
71 ssize_t count = write(channel, out->data + sent, amount);
72 if (count < 0 && errno == EINTR) continue;
73 if (count < 0 && errno == EAGAIN) {
74 write_watch = g_unix_fd_add(channel, G_IO_OUT | G_IO_HUP | G_IO_ERR, flush_output, NULL);
75 return G_SOURCE_REMOVE;
76 }
77 if (count <= 0) { close_channel(); return G_SOURCE_REMOVE; }
78 sent += (size_t)count;
79 }
80 g_byte_array_set_size(out, 0); sent = 0;
81 if (stopping) close_channel();
82 return G_SOURCE_REMOVE;
83}
84
85static int packet(uint32_t type, const uint32_t *words, uint32_t count, const uint8_t *data, size_t length) {
86 if (channel < 0 || stopping) return 0;
87 size_t payload = count * 4 + length;
88 if (payload > SG_MAX_PACKET_BYTES || out->len - sent + payload + 12 > SG_MAX_PACKET_BYTES + 131072u) {
89 close_channel(); return 0;
90 }
91 uint8_t header[12 + SG_SCREEN_WORDS * 4];
92 memcpy(header, SG_MAGIC, 4); sg_word(header + 4, type); sg_word(header + 8, (uint32_t)payload);
93 for (uint32_t i = 0; i < count; ++i) sg_word(header + 12 + i * 4, words[i]);
94 if (sent) { g_byte_array_remove_range(out, 0, (guint)sent); sent = 0; }
95 if (!out->len) queued_at = g_get_monotonic_time();
96 g_byte_array_append(out, header, 12 + count * 4);
97 if (length) g_byte_array_append(out, data, (guint)length);
98 if (!write_watch) flush_output(channel, 0, NULL);
99 return channel >= 0;
100}
101
102static void capabilities(void) {
103 const uint32_t words[SG_CAPABILITIES_WORDS] = {0, 1};
104 packet(SG_CAPABILITIES, words, SG_CAPABILITIES_WORDS, NULL, 0);
105}
106
107static void replay_cursor(void) {
108 if (cursor_packet->len) {
109 uint32_t words[SG_CURSOR_WORDS];
110 for (uint32_t i = 0; i < SG_CURSOR_WORDS; ++i) {
111 const uint8_t *p = cursor_packet->data + i * 4;
112 words[i] = ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
113 }
114 packet(SG_CURSOR, words, SG_CURSOR_WORDS, cursor_packet->data + sizeof(words), cursor_packet->len - sizeof(words));
115 packet(SG_POINTER, pointer, SG_POINTER_WORDS, NULL, 0);
116 }
117}
118
119static void cursor_shape(uint32_t w, uint32_t h, int hot_x, int hot_y, const uint8_t *rgba) {
120 uint32_t words[SG_CURSOR_WORDS] = {0};
121 words[SG_CURSOR_ID] = ++cursor_id;
122 size_t pixels = (size_t)w * h;
123 if (w && h && w <= SG_MAX_CURSOR_SIZE && h <= SG_MAX_CURSOR_SIZE && hot_x >= 0 && hot_y >= 0 && (uint32_t)hot_x < w && (uint32_t)hot_y < h) {
124 words[SG_CURSOR_WIDTH] = w; words[SG_CURSOR_HEIGHT] = h;
125 words[SG_CURSOR_HOT_X] = (uint32_t)hot_x; words[SG_CURSOR_HOT_Y] = (uint32_t)hot_y; words[SG_CURSOR_FRAMES] = 1;
126 } else pixels = 0;
127 g_byte_array_set_size(cursor_packet, sizeof(words) + (pixels ? 4 + pixels * 5 : 0));
128 for (uint32_t i = 0; i < SG_CURSOR_WORDS; ++i) sg_word(cursor_packet->data + i * 4, words[i]);
129 if (pixels) {
130 sg_word(cursor_packet->data + sizeof(words), 1000);
131 memcpy(cursor_packet->data + sizeof(words) + 4, rgba, pixels * 4);
132 memset(cursor_packet->data + sizeof(words) + 4 + pixels * 4, 0, pixels);
133 }
134 pointer[SG_POINTER_ID] = cursor_id;
135 replay_cursor();
136}
137
138static void position(int x, int y, int visible) {
139 uint32_t words[SG_POINTER_WORDS] = {cursor_id, (uint32_t)x, (uint32_t)y, (uint32_t)visible};
140 if (memcmp(pointer, words, sizeof(words))) {
141 memcpy(pointer, words, sizeof(words));
142 packet(SG_POINTER, words, SG_POINTER_WORDS, NULL, 0);
143 }
144}
145
146static void screen_frame(const uint8_t *pixels, uint32_t w, uint32_t h) {
147 if (!w || !h || w > SG_MAX_DIMENSION || h > SG_MAX_DIMENSION || w * h > SG_MAX_PIXELS) return;
148 if (out->len - sent > 65536 || channel < 0 || !capturing || suspended) return;
149 gint64 start = g_get_monotonic_time();
150 if (w != width || h != height) {
151 free(before); before = malloc((size_t)w * h * 4);
152 if (!before) { close_channel(); return; }
153 width = w; height = h; initial = 1;
154 }
155 struct sg_rect rect = {0, 0, w, h};
156 if (!initial && !sg_difference(before, pixels, w, h, &rect)) return;
157 stats[SG_CAPTURE_STATS_SCAN_US] += (uint32_t)(g_get_monotonic_time() - start);
158 start = g_get_monotonic_time();
159 uint32_t words[SG_SCREEN_WORDS] = {w, h, rect.x, rect.y, rect.width, rect.height};
160 size_t length = (size_t)rect.width * rect.height * 4;
161 uint8_t *packed = malloc(length);
162 if (!packed) { close_channel(); return; }
163 for (uint32_t y = 0; y < rect.height; ++y) {
164 size_t offset = ((size_t)(rect.y + y) * w + rect.x) * 4;
165 memcpy(packed + (size_t)y * rect.width * 4, pixels + offset, rect.width * 4);
166 memcpy(before + offset, pixels + offset, rect.width * 4);
167 }
168 if (initial) replay_cursor();
169 if (packet(SG_SCREEN, words, SG_SCREEN_WORDS, packed, length)) initial = 0;
170 free(packed);
171 stats[SG_CAPTURE_STATS_WRITE_US] += (uint32_t)(g_get_monotonic_time() - start);
172}
173
174static gboolean read_requests(gint fd, GIOCondition condition, gpointer unused) {
175 (void)unused;
176 if (condition & (G_IO_HUP | G_IO_ERR)) { read_watch = 0; close_channel(); return G_SOURCE_REMOVE; }
177 for (;;) {
178 ssize_t count = read(fd, request + received, request_size - received);
179 if (count < 0 && errno == EINTR) continue;
180 if (count < 0 && errno == EAGAIN) return G_SOURCE_CONTINUE;
181 if (count <= 0) break;
182 received += (size_t)count;
183 if (received < request_size) continue;
184 if (request_size == 12) {
185 if (memcmp(request, SG_MAGIC, 4) || request[4] || request[5] || request[6] || request[8] || request[9] || request[10]) break;
186 if ((request[7] == SG_STREAM || request[7] == SG_EXPERIMENT) && request[11] == 4) { request_size = 16; continue; }
187 if (request[11]) break;
188 if (request[7] == SG_STOP) {
189 read_watch = 0;
190 capturing = 0;
191 if (pw_stream) pw_stream_set_active(pw_stream, false);
192 packet(SG_STOP, NULL, 0, NULL, 0);
193 stopping = 1;
194 if (!out->len) close_channel();
195 return G_SOURCE_REMOVE;
196 }
197 if (request[7] != SG_REFRESH) break;
198 initial = 1; cursor_changed = 1; capabilities(); replay_cursor();
199 } else {
200 if (request[12] || request[13] || request[14] || request[15] > 1) break;
201 if (request[7] == SG_STREAM) {
202 int enabled = request[15];
203 if (capturing != enabled) {
204 capturing = enabled; initial = 1; cursor_changed = 1;
205 if (enabled) replay_cursor();
206 if (pw_stream) pw_stream_set_active(pw_stream, capturing && !suspended);
207 }
208 }
209 }
210 received = 0; request_size = 12;
211 if (channel < 0) { read_watch = 0; return G_SOURCE_REMOVE; }
212 }
213 read_watch = 0; close_channel(); return G_SOURCE_REMOVE;
214}
215
216static int x_error_handler(Display *d, XErrorEvent *e) { (void)d; (void)e; x_error = 1; return 0; }
217
218static void free_ximage(void) {
219 if (shm_attached) { XShmDetach(display, &shared); XSync(display, False); shm_attached = 0; }
220 if (ximage) {
221 if (shared.shmid >= 0) ximage->data = NULL;
222 XDestroyImage(ximage); ximage = NULL;
223 }
224 if (shared.shmid >= 0) { shmdt(shared.shmaddr); shmctl(shared.shmid, IPC_RMID, NULL); shared.shmid = -1; }
225}
226
227static void x_cursor(void) {
228 XFixesCursorImage *image = XFixesGetCursorImage(display);
229 if (!image) return;
230 if (cursor_changed || image->cursor_serial != cursor_serial) {
231 uint8_t rgba[SG_MAX_CURSOR_SIZE * SG_MAX_CURSOR_SIZE * 4];
232 uint32_t w = image->width, h = image->height;
233 int visible = 0;
234 if (w <= SG_MAX_CURSOR_SIZE && h <= SG_MAX_CURSOR_SIZE) {
235 for (size_t i = 0; i < (size_t)w * h; ++i) {
236 uint32_t pixel = (uint32_t)image->pixels[i];
237 unsigned alpha = pixel >> 24;
238 visible |= alpha != 0;
239 rgba[i * 4 + 3] = (uint8_t)alpha;
240 for (unsigned c = 0; c < 3; ++c) {
241 unsigned value = (pixel >> (16 - c * 8)) & 255;
242 value = alpha ? (value * 255 + alpha / 2) / alpha : 0;
243 rgba[i * 4 + c] = (uint8_t)(value > 255 ? 255 : value);
244 }
245 }
246 position(image->x, image->y, visible);
247 cursor_shape(w, h, image->xhot, image->yhot, rgba);
248 } else { position(image->x, image->y, 1); cursor_shape(0, 0, 0, 0, NULL); }
249 cursor_serial = image->cursor_serial; cursor_changed = 0;
250 } else position(image->x, image->y, pointer[SG_POINTER_VISIBLE]);
251 XFree(image);
252}
253
254static gboolean x_tick(gpointer unused) {
255 (void)unused;
256 while (XPending(display)) {
257 XEvent event; XNextEvent(display, &event);
258 if (event.type == cursor_event + XFixesCursorNotify) cursor_changed = 1;
259 }
260 if (!capturing || suspended || channel < 0 || stopping) return G_SOURCE_CONTINUE;
261 x_cursor();
262 gint64 now = g_get_monotonic_time();
263 if (now < next_capture || out->len - sent > 65536) return G_SOURCE_CONTINUE;
264 next_capture = now + 16667;
265 XWindowAttributes attr;
266 x_error = 0;
267 if (!XGetWindowAttributes(display, root, &attr) || attr.width <= 0 || attr.height <= 0 || attr.width > (int)SG_MAX_DIMENSION || attr.height > (int)SG_MAX_DIMENSION || (uint32_t)attr.width * attr.height > SG_MAX_PIXELS) return G_SOURCE_CONTINUE;
268 if (ximage && (ximage->width != attr.width || ximage->height != attr.height)) free_ximage();
269 if (!ximage && XShmQueryExtension(display)) {
270 ximage = XShmCreateImage(display, attr.visual, (unsigned)attr.depth, ZPixmap, NULL, &shared, (unsigned)attr.width, (unsigned)attr.height);
271 if (ximage) {
272 shared.shmid = shmget(IPC_PRIVATE, (size_t)ximage->bytes_per_line * ximage->height, IPC_CREAT | 0600);
273 if (shared.shmid >= 0) {
274 shared.shmaddr = shmat(shared.shmid, NULL, 0); shared.readOnly = False;
275 if (shared.shmaddr != (void *)-1) {
276 ximage->data = shared.shmaddr;
277 shm_attached = XShmAttach(display, &shared); XSync(display, False);
278 if (x_error) shm_attached = 0;
279 shmctl(shared.shmid, IPC_RMID, NULL);
280 }
281 }
282 if (!shm_attached) free_ximage();
283 }
284 }
285 int temporary = !ximage;
286 XImage *image = ximage;
287 if (image) { if (!XShmGetImage(display, root, image, 0, 0, AllPlanes)) return G_SOURCE_CONTINUE; }
288 else image = XGetImage(display, root, 0, 0, (unsigned)attr.width, (unsigned)attr.height, AllPlanes, ZPixmap);
289 if (!image || x_error) { if (temporary && image) XDestroyImage(image); return G_SOURCE_CONTINUE; }
290 ++stats[SG_CAPTURE_STATS_FRAMES];
291 stats[SG_CAPTURE_STATS_CAPTURE_US] += (uint32_t)(g_get_monotonic_time() - now);
292 if (image->bits_per_pixel == 32 && image->byte_order == LSBFirst && image->red_mask == 0xff0000 && image->green_mask == 0xff00 && image->blue_mask == 0xff && image->bytes_per_line == image->width * 4) {
293 screen_frame((uint8_t *)image->data, (uint32_t)image->width, (uint32_t)image->height);
294 } else {
295 uint8_t *pixels = malloc((size_t)image->width * image->height * 4);
296 if (pixels) {
297 unsigned shifts[3] = {0}, ranges[3];
298 unsigned long masks[3] = {image->blue_mask, image->green_mask, image->red_mask};
299 for (unsigned c = 0; c < 3; ++c) {
300 while (masks[c] && !(masks[c] & 1)) { ++shifts[c]; masks[c] >>= 1; }
301 ranges[c] = (unsigned)masks[c];
302 }
303 for (int y = 0; y < image->height; ++y) for (int x = 0; x < image->width; ++x) {
304 unsigned long pixel = XGetPixel(image, x, y);
305 size_t offset = ((size_t)y * image->width + x) * 4;
306 for (unsigned c = 0; c < 3; ++c) pixels[offset + c] = ranges[c] ? (uint8_t)(((pixel >> shifts[c]) & ranges[c]) * 255 / ranges[c]) : 0;
307 pixels[offset + 3] = 0;
308 }
309 screen_frame(pixels, (uint32_t)image->width, (uint32_t)image->height); free(pixels);
310 }
311 }
312 if (temporary) XDestroyImage(image);
313 return G_SOURCE_CONTINUE;
314}
315
316static void pw_cursor(struct spa_buffer *buffer) {
317 struct spa_meta *meta = spa_buffer_find_meta(buffer, SPA_META_Cursor);
318 if (!meta || meta->size < sizeof(struct spa_meta_cursor)) return;
319 struct spa_meta_cursor cursor; memcpy(&cursor, meta->data, sizeof(cursor));
320 position(cursor.position.x, cursor.position.y, cursor.id != 0);
321 if (!cursor.id) return;
322 if (!cursor.bitmap_offset) return;
323 if (cursor.bitmap_offset < sizeof(cursor) || cursor.bitmap_offset > meta->size - sizeof(struct spa_meta_bitmap)) return;
324 struct spa_meta_bitmap bitmap; memcpy(&bitmap, (uint8_t *)meta->data + cursor.bitmap_offset, sizeof(bitmap));
325 if (!bitmap.offset || !bitmap.size.width || !bitmap.size.height) {
326 cursor_shape(1, 1, 0, 0, (uint8_t[4]){0}); return;
327 }
328 if (!bitmap.format) return;
329 uint32_t w = bitmap.size.width, h = bitmap.size.height;
330 size_t available = meta->size - cursor.bitmap_offset;
331 if (w > SG_MAX_CURSOR_SIZE || h > SG_MAX_CURSOR_SIZE || bitmap.stride < (int32_t)w * 4 || bitmap.offset < sizeof(bitmap) || bitmap.offset > available || (size_t)(h - 1) * bitmap.stride + w * 4 > available - bitmap.offset) {
332 cursor_shape(0, 0, 0, 0, NULL); return;
333 }
334 unsigned offsets[4];
335 switch (bitmap.format) {
336 case SPA_VIDEO_FORMAT_RGBA: offsets[0] = 0; offsets[1] = 1; offsets[2] = 2; offsets[3] = 3; break;
337 case SPA_VIDEO_FORMAT_BGRA: offsets[0] = 2; offsets[1] = 1; offsets[2] = 0; offsets[3] = 3; break;
338 case SPA_VIDEO_FORMAT_ARGB: offsets[0] = 1; offsets[1] = 2; offsets[2] = 3; offsets[3] = 0; break;
339 case SPA_VIDEO_FORMAT_ABGR: offsets[0] = 3; offsets[1] = 2; offsets[2] = 1; offsets[3] = 0; break;
340 default: cursor_shape(0, 0, 0, 0, NULL); return;
341 }
342 uint8_t rgba[SG_MAX_CURSOR_SIZE * SG_MAX_CURSOR_SIZE * 4];
343 const uint8_t *pixels = (uint8_t *)meta->data + cursor.bitmap_offset + bitmap.offset;
344 for (uint32_t y = 0; y < h; ++y) for (uint32_t x = 0; x < w; ++x) {
345 const uint8_t *src = pixels + (size_t)y * bitmap.stride + x * 4;
346 uint8_t *dst = rgba + ((size_t)y * w + x) * 4;
347 unsigned alpha = src[offsets[3]]; dst[3] = (uint8_t)alpha;
348 for (unsigned c = 0; c < 3; ++c) {
349 unsigned value = alpha ? (src[offsets[c]] * 255u + alpha / 2) / alpha : 0;
350 dst[c] = (uint8_t)(value > 255 ? 255 : value);
351 }
352 }
353 cursor_shape(w, h, cursor.hotspot.x, cursor.hotspot.y, rgba);
354}
355
356static void pw_process(void *unused) {
357 (void)unused;
358 struct pw_buffer *current = NULL, *next;
359 while ((next = pw_stream_dequeue_buffer(pw_stream))) {
360 /* Cursor updates can exist on a buffer whose video is superseded. */
361 if (capturing && !suspended) pw_cursor(next->buffer);
362 if (current) pw_stream_queue_buffer(pw_stream, current);
363 current = next;
364 }
365 if (!current) return;
366 struct spa_buffer *buffer = current->buffer;
367 uint32_t w = video_info.size.width, h = video_info.size.height;
368 if (capturing && !suspended && buffer->n_datas && w && h && w <= SG_MAX_DIMENSION && h <= SG_MAX_DIMENSION && w * h <= SG_MAX_PIXELS) {
369 struct spa_data *data = &buffer->datas[0];
370 struct spa_meta_videotransform *transform = spa_buffer_find_meta_data(buffer, SPA_META_VideoTransform, sizeof(*transform));
371 if (transform && transform->transform != SPA_META_TRANSFORMATION_None) {
372 packet(SG_SUSPEND, NULL, 0, NULL, 0); initial = 1;
373 } else if (data->data && data->chunk && !(data->chunk->flags & SPA_CHUNK_FLAG_CORRUPTED)) {
374 int32_t stride = data->chunk->stride;
375 size_t offset = data->chunk->offset;
376 size_t length = stride > 0 ? (size_t)(h - 1) * stride + w * 4 : 0;
377 if (stride >= (int32_t)w * 4 && offset <= data->maxsize && length <= data->maxsize - offset && length <= data->chunk->size) {
378 gint64 start = g_get_monotonic_time();
379 uint8_t *pixels = malloc((size_t)w * h * 4);
380 if (pixels) {
381 for (uint32_t y = 0; y < h; ++y) memcpy(pixels + (size_t)y * w * 4, (uint8_t *)data->data + offset + (size_t)y * stride, w * 4);
382 ++stats[SG_CAPTURE_STATS_FRAMES];
383 stats[SG_CAPTURE_STATS_CAPTURE_US] += (uint32_t)(g_get_monotonic_time() - start);
384 screen_frame(pixels, w, h); free(pixels);
385 }
386 }
387 }
388 }
389 pw_stream_queue_buffer(pw_stream, current);
390}
391
392static void pw_format(void *unused, uint32_t id, const struct spa_pod *param) {
393 (void)unused;
394 if (id != SPA_PARAM_Format || !param) return;
395 memset(&video_info, 0, sizeof(video_info));
396 if (spa_format_video_raw_parse(param, &video_info) < 0 || video_info.format != SPA_VIDEO_FORMAT_BGRx) { close_channel(); return; }
397 uint8_t memory[1024]; struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(memory, sizeof(memory));
398 const struct spa_pod *params[3];
399 params[0] = spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
400 SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(3, 2, 8),
401 SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1 << SPA_DATA_MemPtr) | (1 << SPA_DATA_MemFd)));
402 params[1] = spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
403 SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor),
404 /* Compositors can require a larger allocation than the transmitted cursor limit. */
405 SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(
406 sizeof(struct spa_meta_cursor) + sizeof(struct spa_meta_bitmap) + SG_MAX_CURSOR_SIZE * SG_MAX_CURSOR_SIZE * 4,
407 sizeof(struct spa_meta_cursor) + sizeof(struct spa_meta_bitmap) + 4,
408 sizeof(struct spa_meta_cursor) + sizeof(struct spa_meta_bitmap) + 384 * 384 * 4));
409 params[2] = spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
410 SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoTransform), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_videotransform)));
411 pw_stream_update_params(pw_stream, params, 3);
412 initial = 1;
413}
414
415static void capture_failed(void) {
416 if (!g_main_loop_is_running(main_loop)) return;
417 packet(SG_SUSPEND, NULL, 0, NULL, 0);
418 restart = 1; g_main_loop_quit(main_loop);
419}
420
421static void pw_state(void *unused, enum pw_stream_state old, enum pw_stream_state state, const char *error) {
422 (void)unused; (void)old;
423 if (state == PW_STREAM_STATE_ERROR || state == PW_STREAM_STATE_UNCONNECTED) {
424 if (error) g_printerr("PipeWire: %s\n", error);
425 capture_failed();
426 }
427}
428
429static const struct pw_stream_events stream_events = {
430 PW_VERSION_STREAM_EVENTS, .state_changed = pw_state, .param_changed = pw_format, .process = pw_process,
431};
432
433static gboolean pw_dispatch(gint fd, GIOCondition condition, gpointer unused) {
434 (void)fd; (void)unused;
435 if ((condition & (G_IO_HUP | G_IO_ERR)) || pw_loop_iterate(pw_main_loop_get_loop(pw_loop), 0) < 0) {
436 capture_failed(); pw_watch = 0; return G_SOURCE_REMOVE;
437 }
438 return G_SOURCE_CONTINUE;
439}
440
441static void portal_call(const char *method, GVariantBuilder *options);
442
443static void portal_closed(GDBusConnection *connection, const char *sender, const char *path, const char *interface, const char *signal, GVariant *parameters, gpointer unused) {
444 (void)connection; (void)sender; (void)path; (void)interface; (void)signal; (void)parameters; (void)unused;
445 if (pw_stream) pw_stream_set_active(pw_stream, false);
446 capture_failed();
447 g_clear_pointer(&session_path, g_free);
448}
449
450static void portal_response(GDBusConnection *connection, const char *sender, const char *path, const char *interface, const char *signal, GVariant *parameters, gpointer unused) {
451 (void)connection; (void)sender; (void)interface; (void)signal; (void)unused;
452 if (!request_path || strcmp(path, request_path)) return;
453 guint response; GVariant *results;
454 g_variant_get(parameters, "(u@a{sv})", &response, &results);
455 g_clear_pointer(&request_path, g_free);
456 if (response) { g_printerr("ScreenCast request ended: %u\n", response); portal_step = -1; if (suspended) capture_failed(); g_variant_unref(results); return; }
457 GVariantBuilder options; g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
458 if (portal_step == 0) {
459 const char *session;
460 if (!g_variant_lookup(results, "session_handle", "&s", &session)) { capture_failed(); goto done; }
461 session_path = g_strdup(session);
462 session_subscription = g_dbus_connection_signal_subscribe(bus, "org.freedesktop.portal.Desktop", "org.freedesktop.portal.Session", "Closed", session_path, NULL, G_DBUS_SIGNAL_FLAGS_NONE, portal_closed, NULL, NULL);
463 g_variant_builder_add(&options, "{sv}", "types", g_variant_new_uint32(1));
464 g_variant_builder_add(&options, "{sv}", "multiple", g_variant_new_boolean(false));
465 g_variant_builder_add(&options, "{sv}", "cursor_mode", g_variant_new_uint32(4));
466 g_variant_builder_add(&options, "{sv}", "persist_mode", g_variant_new_uint32(2));
467 char *token = NULL;
468 if (g_file_get_contents(restore_path, &token, NULL, NULL) && *token) g_variant_builder_add(&options, "{sv}", "restore_token", g_variant_new_string(token));
469 g_free(token); ++portal_step;
470 portal_call("SelectSources", &options);
471 } else if (portal_step == 1) {
472 ++portal_step;
473 portal_call("Start", &options);
474 } else {
475 GVariant *streams = g_variant_lookup_value(results, "streams", G_VARIANT_TYPE("a(ua{sv})"));
476 if (!streams || g_variant_n_children(streams) != 1) { if (streams) g_variant_unref(streams); capture_failed(); goto done; }
477 guint node; GVariant *properties;
478 g_variant_get_child(streams, 0, "(u@a{sv})", &node, &properties);
479 guint64 serial = 0; g_variant_lookup(properties, "pipewire-serial", "t", &serial);
480 char target[32]; g_snprintf(target, sizeof(target), "%" G_GUINT64_FORMAT, serial);
481 const char *token;
482 if (g_variant_lookup(results, "restore_token", "&s", &token)) {
483 char *directory = g_path_get_dirname(restore_path);
484 if (!g_mkdir_with_parents(directory, 0700)) {
485 int fd = open(restore_path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0600);
486 if (fd >= 0) { if (write(fd, token, strlen(token)) < 0) g_printerr("Could not save ScreenCast token\n"); close(fd); }
487 }
488 g_free(directory);
489 }
490 GUnixFDList *fds = NULL; GError *error = NULL;
491 GVariant *reply = g_dbus_connection_call_with_unix_fd_list_sync(bus, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.ScreenCast", "OpenPipeWireRemote",
492 g_variant_new("(o@a{sv})", session_path, g_variant_new_array(G_VARIANT_TYPE("{sv}"), NULL, 0)), G_VARIANT_TYPE("(h)"), G_DBUS_CALL_FLAGS_NONE, 5000, NULL, &fds, NULL, &error);
493 if (reply) {
494 int index; g_variant_get(reply, "(h)", &index);
495 int fd = g_unix_fd_list_get(fds, index, &error);
496 if (fd >= 0) {
497 pw_loop = pw_main_loop_new(NULL);
498 if (pw_loop) pw_loop_enter(pw_main_loop_get_loop(pw_loop));
499 pw_context = pw_loop ? pw_context_new(pw_main_loop_get_loop(pw_loop), NULL, 0) : NULL;
500 if (pw_context) pw_core = pw_context_connect_fd(pw_context, fd, NULL, 0);
501 else close(fd);
502 if (pw_core) {
503 struct pw_properties *settings = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", NULL);
504 if (serial) pw_properties_set(settings, PW_KEY_TARGET_OBJECT, target);
505 pw_stream = pw_stream_new(pw_core, "Snowglobe guest", settings);
506 }
507 if (pw_stream) {
508 pw_stream_add_listener(pw_stream, &stream_listener, &stream_events, NULL);
509 uint8_t memory[512]; struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(memory, sizeof(memory));
510 const struct spa_pod *format = spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
511 SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
512 SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_BGRx),
513 SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(&SPA_RECTANGLE(1280, 720), &SPA_RECTANGLE(1, 1), &SPA_RECTANGLE(SG_MAX_DIMENSION, SG_MAX_DIMENSION)),
514 SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(&SPA_FRACTION(60, 1), &SPA_FRACTION(0, 1), &SPA_FRACTION(60, 1)));
515 if (pw_stream_connect(pw_stream, PW_DIRECTION_INPUT, serial ? PW_ID_ANY : node, PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_INACTIVE, &format, 1) >= 0) {
516 pw_watch = g_unix_fd_add(pw_loop_get_fd(pw_main_loop_get_loop(pw_loop)), G_IO_IN | G_IO_ERR | G_IO_HUP, pw_dispatch, NULL);
517 pw_stream_set_active(pw_stream, capturing && !suspended);
518 }
519 }
520 }
521 g_variant_unref(reply); g_object_unref(fds);
522 }
523 if (error) { g_printerr("ScreenCast remote: %s\n", error->message); g_error_free(error); }
524 g_variant_unref(properties); g_variant_unref(streams);
525 if (!pw_watch) capture_failed();
526 }
527done:
528 g_variant_unref(results);
529}
530
531static void portal_called(GObject *object, GAsyncResult *result, gpointer unused) {
532 (void)unused;
533 GError *error = NULL;
534 GVariant *reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(object), result, &error);
535 if (reply) g_variant_unref(reply);
536 if (error) { g_printerr("ScreenCast: %s\n", error->message); g_error_free(error); g_clear_pointer(&request_path, g_free); capture_failed(); }
537}
538
539static void portal_call(const char *method, GVariantBuilder *options) {
540 static unsigned sequence;
541 char token[64]; g_snprintf(token, sizeof(token), "snowglobe_%u_%u", (unsigned)getpid(), ++sequence);
542 const char *sender = g_dbus_connection_get_unique_name(bus);
543 char *name = g_strdup(sender + 1);
544 for (char *p = name; *p; ++p) if (*p == '.') *p = '_';
545 request_path = g_strdup_printf("/org/freedesktop/portal/desktop/request/%s/%s", name, token); g_free(name);
546 g_variant_builder_add(options, "{sv}", "handle_token", g_variant_new_string(token));
547 GVariant *args;
548 if (!portal_step) args = g_variant_new("(@a{sv})", g_variant_builder_end(options));
549 else if (portal_step == 1) args = g_variant_new("(o@a{sv})", session_path, g_variant_builder_end(options));
550 else args = g_variant_new("(os@a{sv})", session_path, "", g_variant_builder_end(options));
551 g_dbus_connection_call(bus, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.ScreenCast", method, args, G_VARIANT_TYPE("(o)"), G_DBUS_CALL_FLAGS_NONE, 5000, NULL, portal_called, NULL);
552}
553
554static void login_state(GVariant *properties) {
555 gboolean value;
556 if (g_variant_lookup(properties, "Active", "b", &value)) session_active = value;
557 if (g_variant_lookup(properties, "LockedHint", "b", &value)) session_locked = value;
558 int blocked = !session_active || session_locked;
559 if (blocked == suspended) return;
560 suspended = blocked; initial = 1; cursor_changed = 1;
561 if (blocked) { packet(SG_SUSPEND, NULL, 0, NULL, 0); close_channel(); }
562 else { capabilities(); replay_cursor(); }
563 if (pw_stream) pw_stream_set_active(pw_stream, capturing && !suspended);
564}
565
566static void login_changed(GDBusConnection *connection, const char *sender, const char *path, const char *interface, const char *signal, GVariant *parameters, gpointer unused) {
567 (void)connection; (void)sender; (void)path; (void)interface; (void)signal; (void)unused;
568 const char *name; GVariant *properties, *invalid;
569 g_variant_get(parameters, "(&s@a{sv}@as)", &name, &properties, &invalid);
570 if (!strcmp(name, "org.freedesktop.login1.Session")) login_state(properties);
571 g_variant_unref(properties); g_variant_unref(invalid);
572}
573
574static gboolean heartbeat(gpointer unused) {
575 (void)unused;
576 if (suspended) { stats_at = g_get_monotonic_time(); memset(stats, 0, sizeof(stats)); return G_SOURCE_CONTINUE; }
577 if (bus && !portal_step && !request_path && !session_path) {
578 GVariantBuilder options; g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
579 char token[64]; g_snprintf(token, sizeof(token), "snowglobe_session_%u", (unsigned)getpid());
580 g_variant_builder_add(&options, "{sv}", "session_handle_token", g_variant_new_string(token));
581 portal_call("CreateSession", &options);
582 }
583 if (out->len && g_get_monotonic_time() - queued_at >= 3000000) close_channel();
584 if (channel < 0) {
585 channel = open(channel_path, O_RDWR | O_NONBLOCK | O_CLOEXEC);
586 if (channel >= 0) {
587 if (flock(channel, LOCK_EX | LOCK_NB)) { close(channel); channel = -1; return G_SOURCE_CONTINUE; }
588 read_watch = g_unix_fd_add(channel, G_IO_IN | G_IO_ERR | G_IO_HUP, read_requests, NULL);
589 capabilities();
590 }
591 }
592 if (channel >= 0 && !stopping) {
593 gint64 now = g_get_monotonic_time();
594 stats[SG_CAPTURE_STATS_ELAPSED_MS] = (uint32_t)((now - stats_at) / 1000);
595 packet(SG_CAPTURE_STATS, stats, SG_CAPTURE_STATS_WORDS, NULL, 0);
596 memset(stats, 0, sizeof(stats)); stats_at = now;
597 packet(SG_HEARTBEAT, NULL, 0, NULL, 0);
598 } else stats_at = g_get_monotonic_time();
599 return G_SOURCE_CONTINUE;
600}
601
602static gboolean quit(gpointer unused) { (void)unused; g_main_loop_quit(main_loop); return G_SOURCE_REMOVE; }
603
604int main(int argc, char **argv) {
605 if (argc > 2) return 2;
606 channel_path = argc == 2 ? argv[1] : "/dev/virtio-ports/" SG_CHANNEL_NAME;
607 signal(SIGPIPE, SIG_IGN);
608 pw_init(NULL, NULL);
609 main_loop = g_main_loop_new(NULL, false);
610 out = g_byte_array_new(); cursor_packet = g_byte_array_new(); stats_at = g_get_monotonic_time();
611 pointer[SG_POINTER_VISIBLE] = 1;
612 if (getenv("WAYLAND_DISPLAY") || (getenv("XDG_SESSION_TYPE") && !strcmp(getenv("XDG_SESSION_TYPE"), "wayland"))) {
613 bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
614 if (!bus) { restart = 1; goto cleanup; }
615 restore_path = g_build_filename(g_get_user_config_dir(), "snowglobe", "screencast-token", NULL);
616 GVariant *reply = g_dbus_connection_call_sync(bus, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.DBus.Properties", "Get",
617 g_variant_new("(ss)", "org.freedesktop.portal.ScreenCast", "AvailableCursorModes"), G_VARIANT_TYPE("(v)"), G_DBUS_CALL_FLAGS_NONE, 5000, NULL, NULL);
618 if (!reply) { restart = 1; goto cleanup; }
619 guint modes = 0;
620 GVariant *value; g_variant_get(reply, "(v)", &value); if (g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32)) modes = g_variant_get_uint32(value); g_variant_unref(value); g_variant_unref(reply);
621 if (!(modes & 4)) { g_printerr("ScreenCast cursor metadata is unavailable\n"); return 1; }
622 portal_subscription = g_dbus_connection_signal_subscribe(bus, "org.freedesktop.portal.Desktop", "org.freedesktop.portal.Request", "Response", NULL, NULL, G_DBUS_SIGNAL_FLAGS_NONE, portal_response, NULL, NULL);
623 } else {
624 display = XOpenDisplay(NULL);
625 if (!display || !XFixesQueryExtension(display, &cursor_event, &(int){0})) return 1;
626 int major = 2, minor = 0;
627 if (!XFixesQueryVersion(display, &major, &minor) || major < 2) return 1;
628 XSetErrorHandler(x_error_handler); root = DefaultRootWindow(display);
629 XFixesSelectCursorInput(display, root, XFixesDisplayCursorNotifyMask);
630 g_timeout_add(4, x_tick, NULL);
631 }
632 login_bus = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL);
633 if (login_bus) {
634 GVariant *reply = g_dbus_connection_call_sync(login_bus, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "GetUser",
635 g_variant_new("(u)", (guint)getuid()), G_VARIANT_TYPE("(o)"), G_DBUS_CALL_FLAGS_NONE, 2000, NULL, NULL);
636 if (reply) {
637 const char *user_path; g_variant_get(reply, "(&o)", &user_path);
638 GVariant *property = g_dbus_connection_call_sync(login_bus, "org.freedesktop.login1", user_path, "org.freedesktop.DBus.Properties", "Get",
639 g_variant_new("(ss)", "org.freedesktop.login1.User", "Display"), G_VARIANT_TYPE("(v)"), G_DBUS_CALL_FLAGS_NONE, 2000, NULL, NULL);
640 g_variant_unref(reply); reply = NULL;
641 if (property) {
642 GVariant *value; g_variant_get(property, "(v)", &value);
643 const char *id, *path; g_variant_get(value, "(&s&o)", &id, &path);
644 if (*id) reply = g_variant_ref_sink(g_variant_new("(o)", path));
645 g_variant_unref(value); g_variant_unref(property);
646 }
647 }
648 if (reply) {
649 const char *path; g_variant_get(reply, "(&o)", &path);
650 g_dbus_connection_signal_subscribe(login_bus, "org.freedesktop.login1", "org.freedesktop.DBus.Properties", "PropertiesChanged", path, NULL, G_DBUS_SIGNAL_FLAGS_NONE, login_changed, NULL, NULL);
651 GVariant *property = g_dbus_connection_call_sync(login_bus, "org.freedesktop.login1", path, "org.freedesktop.DBus.Properties", "GetAll",
652 g_variant_new("(s)", "org.freedesktop.login1.Session"), G_VARIANT_TYPE("(a{sv})"), G_DBUS_CALL_FLAGS_NONE, 2000, NULL, NULL);
653 g_variant_unref(reply);
654 if (property) { GVariant *properties; g_variant_get(property, "(@a{sv})", &properties); login_state(properties); g_variant_unref(properties); g_variant_unref(property); }
655 }
656 }
657 g_unix_signal_add(SIGTERM, quit, NULL); g_unix_signal_add(SIGINT, quit, NULL);
658 g_timeout_add(1000, heartbeat, NULL); heartbeat(NULL);
659 g_main_loop_run(main_loop);
660cleanup:
661 close_channel();
662 if (display) { free_ximage(); XCloseDisplay(display); }
663 if (pw_watch) g_source_remove(pw_watch);
664 if (pw_stream) pw_stream_destroy(pw_stream);
665 if (pw_core) pw_core_disconnect(pw_core);
666 if (pw_context) pw_context_destroy(pw_context);
667 if (pw_loop) { pw_loop_leave(pw_main_loop_get_loop(pw_loop)); pw_main_loop_destroy(pw_loop); }
668 if (bus) {
669 if (session_path) g_dbus_connection_call_sync(bus, "org.freedesktop.portal.Desktop", session_path, "org.freedesktop.portal.Session", "Close", NULL, NULL, G_DBUS_CALL_FLAGS_NONE, 2000, NULL, NULL);
670 if (session_subscription) g_dbus_connection_signal_unsubscribe(bus, session_subscription);
671 if (portal_subscription) g_dbus_connection_signal_unsubscribe(bus, portal_subscription);
672 g_object_unref(bus);
673 }
674 if (login_bus) g_object_unref(login_bus);
675 g_free(session_path); g_free(request_path); g_free(restore_path); free(before);
676 g_byte_array_unref(out); g_byte_array_unref(cursor_packet); g_main_loop_unref(main_loop); pw_deinit();
677 if (restart) {
678 signal(SIGTERM, SIG_DFL); signal(SIGINT, SIG_DFL);
679 g_usleep(5000000);
680 execvp(argv[0], argv);
681 return 1;
682 }
683 return 0;
684}