1 /*
2 Copyright (C) 2014 by Leonhard Oelke <leonhard@in-verted.de>
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <inttypes.h>
21 #include <xcb/randr.h>
22 #include <xcb/shm.h>
23 #include <xcb/xfixes.h>
24 #include <xcb/xinerama.h>
25
26 #include <obs-module.h>
27 #include <util/dstr.h>
28 #include "xcursor-xcb.h"
29 #include "xhelpers.h"
30
31 #define XSHM_DATA(voidptr) struct xshm_data *data = voidptr;
32
33 #define blog(level, msg, ...) blog(level, "xshm-input: " msg, ##__VA_ARGS__)
34
35 struct xshm_data {
36 obs_source_t *source;
37
38 xcb_connection_t *xcb;
39 xcb_screen_t *xcb_screen;
40 xcb_shm_t *xshm;
41 xcb_xcursor_t *cursor;
42
43 char *server;
44 uint_fast32_t screen_id;
45 int_fast32_t x_org;
46 int_fast32_t y_org;
47 int_fast32_t width;
48 int_fast32_t height;
49
50 gs_texture_t *texture;
51
52 int_fast32_t cut_top;
53 int_fast32_t cut_left;
54 int_fast32_t cut_right;
55 int_fast32_t cut_bot;
56
57 int_fast32_t adj_x_org;
58 int_fast32_t adj_y_org;
59 int_fast32_t adj_width;
60 int_fast32_t adj_height;
61
62 bool show_cursor;
63 bool use_xinerama;
64 bool use_randr;
65 bool advanced;
66 };
67
68 /**
69 * Resize the texture
70 *
71 * This will automatically create the texture if it does not exist
72 *
73 * @note requires to be called within the obs graphics context
74 */
xshm_resize_texture(struct xshm_data * data)75 static inline void xshm_resize_texture(struct xshm_data *data)
76 {
77 if (data->texture)
78 gs_texture_destroy(data->texture);
79 data->texture = gs_texture_create(data->adj_width, data->adj_height,
80 GS_BGRA, 1, NULL, GS_DYNAMIC);
81 }
82
83 /**
84 * Check if the xserver supports all the extensions we need
85 */
xshm_check_extensions(xcb_connection_t * xcb)86 static bool xshm_check_extensions(xcb_connection_t *xcb)
87 {
88 bool ok = true;
89
90 if (!xcb_get_extension_data(xcb, &xcb_shm_id)->present) {
91 blog(LOG_ERROR, "Missing SHM extension !");
92 ok = false;
93 }
94
95 if (!xcb_get_extension_data(xcb, &xcb_xinerama_id)->present)
96 blog(LOG_INFO, "Missing Xinerama extension !");
97
98 if (!xcb_get_extension_data(xcb, &xcb_randr_id)->present)
99 blog(LOG_INFO, "Missing Randr extension !");
100
101 return ok;
102 }
103
104 /**
105 * Update the capture
106 *
107 * @return < 0 on error, 0 when size is unchanged, > 1 on size change
108 */
xshm_update_geometry(struct xshm_data * data)109 static int_fast32_t xshm_update_geometry(struct xshm_data *data)
110 {
111 int_fast32_t prev_width = data->adj_width;
112 int_fast32_t prev_height = data->adj_height;
113
114 if (data->use_randr) {
115 if (randr_screen_geo(data->xcb, data->screen_id, &data->x_org,
116 &data->y_org, &data->width, &data->height,
117 &data->xcb_screen, NULL) < 0) {
118 return -1;
119 }
120 } else if (data->use_xinerama) {
121 if (xinerama_screen_geo(data->xcb, data->screen_id,
122 &data->x_org, &data->y_org,
123 &data->width, &data->height) < 0) {
124 return -1;
125 }
126 data->xcb_screen = xcb_get_screen(data->xcb, 0);
127 } else {
128 data->x_org = 0;
129 data->y_org = 0;
130 if (x11_screen_geo(data->xcb, data->screen_id, &data->width,
131 &data->height) < 0) {
132 return -1;
133 }
134 data->xcb_screen = xcb_get_screen(data->xcb, data->screen_id);
135 }
136
137 if (!data->width || !data->height) {
138 blog(LOG_ERROR, "Failed to get geometry");
139 return -1;
140 }
141
142 data->adj_y_org = data->y_org;
143 data->adj_x_org = data->x_org;
144 data->adj_height = data->height;
145 data->adj_width = data->width;
146
147 if (data->cut_top != 0) {
148 if (data->y_org > 0)
149 data->adj_y_org = data->y_org + data->cut_top;
150 else
151 data->adj_y_org = data->cut_top;
152 data->adj_height = data->adj_height - data->cut_top;
153 }
154 if (data->cut_left != 0) {
155 if (data->x_org > 0)
156 data->adj_x_org = data->x_org + data->cut_left;
157 else
158 data->adj_x_org = data->cut_left;
159 data->adj_width = data->adj_width - data->cut_left;
160 }
161 if (data->cut_right != 0)
162 data->adj_width = data->adj_width - data->cut_right;
163 if (data->cut_bot != 0)
164 data->adj_height = data->adj_height - data->cut_bot;
165
166 blog(LOG_INFO,
167 "Geometry %" PRIdFAST32 "x%" PRIdFAST32 " @ %" PRIdFAST32
168 ",%" PRIdFAST32,
169 data->width, data->height, data->x_org, data->y_org);
170
171 if (prev_width == data->adj_width && prev_height == data->adj_height)
172 return 0;
173
174 return 1;
175 }
176
177 /**
178 * Returns the name of the plugin
179 */
xshm_getname(void * unused)180 static const char *xshm_getname(void *unused)
181 {
182 UNUSED_PARAMETER(unused);
183 return obs_module_text("X11SharedMemoryScreenInput");
184 }
185
186 /**
187 * Stop the capture
188 */
xshm_capture_stop(struct xshm_data * data)189 static void xshm_capture_stop(struct xshm_data *data)
190 {
191 obs_enter_graphics();
192
193 if (data->texture) {
194 gs_texture_destroy(data->texture);
195 data->texture = NULL;
196 }
197 if (data->cursor) {
198 xcb_xcursor_destroy(data->cursor);
199 data->cursor = NULL;
200 }
201
202 obs_leave_graphics();
203
204 if (data->xshm) {
205 xshm_xcb_detach(data->xshm);
206 data->xshm = NULL;
207 }
208
209 if (data->xcb) {
210 xcb_disconnect(data->xcb);
211 data->xcb = NULL;
212 }
213
214 if (data->server) {
215 bfree(data->server);
216 data->server = NULL;
217 }
218 }
219
220 /**
221 * Start the capture
222 */
xshm_capture_start(struct xshm_data * data)223 static void xshm_capture_start(struct xshm_data *data)
224 {
225 const char *server = (data->advanced && *data->server) ? data->server
226 : NULL;
227
228 data->xcb = xcb_connect(server, NULL);
229 if (!data->xcb || xcb_connection_has_error(data->xcb)) {
230 blog(LOG_ERROR, "Unable to open X display !");
231 goto fail;
232 }
233
234 if (!xshm_check_extensions(data->xcb))
235 goto fail;
236
237 data->use_randr = randr_is_active(data->xcb) ? true : false;
238 data->use_xinerama = xinerama_is_active(data->xcb) ? true : false;
239
240 if (xshm_update_geometry(data) < 0) {
241 blog(LOG_ERROR, "failed to update geometry !");
242 goto fail;
243 }
244
245 data->xshm =
246 xshm_xcb_attach(data->xcb, data->adj_width, data->adj_height);
247 if (!data->xshm) {
248 blog(LOG_ERROR, "failed to attach shm !");
249 goto fail;
250 }
251
252 data->cursor = xcb_xcursor_init(data->xcb);
253 xcb_xcursor_offset(data->cursor, data->adj_x_org, data->adj_y_org);
254
255 obs_enter_graphics();
256
257 xshm_resize_texture(data);
258
259 obs_leave_graphics();
260
261 return;
262 fail:
263 xshm_capture_stop(data);
264 }
265
266 /**
267 * Update the capture with changed settings
268 */
xshm_update(void * vptr,obs_data_t * settings)269 static void xshm_update(void *vptr, obs_data_t *settings)
270 {
271 XSHM_DATA(vptr);
272
273 xshm_capture_stop(data);
274
275 data->screen_id = obs_data_get_int(settings, "screen");
276 data->show_cursor = obs_data_get_bool(settings, "show_cursor");
277 data->advanced = obs_data_get_bool(settings, "advanced");
278 data->server = bstrdup(obs_data_get_string(settings, "server"));
279
280 data->cut_top = obs_data_get_int(settings, "cut_top");
281 data->cut_left = obs_data_get_int(settings, "cut_left");
282 data->cut_right = obs_data_get_int(settings, "cut_right");
283 data->cut_bot = obs_data_get_int(settings, "cut_bot");
284
285 xshm_capture_start(data);
286 }
287
288 /**
289 * Get the default settings for the capture
290 */
xshm_defaults(obs_data_t * defaults)291 static void xshm_defaults(obs_data_t *defaults)
292 {
293 obs_data_set_default_int(defaults, "screen", 0);
294 obs_data_set_default_bool(defaults, "show_cursor", true);
295 obs_data_set_default_bool(defaults, "advanced", false);
296 obs_data_set_default_int(defaults, "cut_top", 0);
297 obs_data_set_default_int(defaults, "cut_left", 0);
298 obs_data_set_default_int(defaults, "cut_right", 0);
299 obs_data_set_default_int(defaults, "cut_bot", 0);
300 }
301
302 /**
303 * Toggle visibility of advanced settings
304 */
xshm_toggle_advanced(obs_properties_t * props,obs_property_t * p,obs_data_t * settings)305 static bool xshm_toggle_advanced(obs_properties_t *props, obs_property_t *p,
306 obs_data_t *settings)
307 {
308 UNUSED_PARAMETER(p);
309 const bool visible = obs_data_get_bool(settings, "advanced");
310 obs_property_t *server = obs_properties_get(props, "server");
311
312 obs_property_set_visible(server, visible);
313
314 /* trigger server changed callback so the screen list is refreshed */
315 obs_property_modified(server, settings);
316
317 return true;
318 }
319
320 /**
321 * The x server was changed
322 */
xshm_server_changed(obs_properties_t * props,obs_property_t * p,obs_data_t * settings)323 static bool xshm_server_changed(obs_properties_t *props, obs_property_t *p,
324 obs_data_t *settings)
325 {
326 UNUSED_PARAMETER(p);
327
328 bool advanced = obs_data_get_bool(settings, "advanced");
329 int_fast32_t old_screen = obs_data_get_int(settings, "screen");
330 const char *server = obs_data_get_string(settings, "server");
331 obs_property_t *screens = obs_properties_get(props, "screen");
332
333 /* we want a real NULL here in case there is no string here */
334 server = (advanced && *server) ? server : NULL;
335
336 obs_property_list_clear(screens);
337
338 xcb_connection_t *xcb = xcb_connect(server, NULL);
339 if (!xcb || xcb_connection_has_error(xcb)) {
340 obs_property_set_enabled(screens, false);
341 return true;
342 }
343
344 struct dstr screen_info;
345 dstr_init(&screen_info);
346 bool randr = randr_is_active(xcb);
347 bool xinerama = xinerama_is_active(xcb);
348 int_fast32_t count =
349 randr ? randr_screen_count(xcb)
350 : (xinerama ? xinerama_screen_count(xcb)
351 : xcb_setup_roots_length(xcb_get_setup(xcb)));
352
353 for (int_fast32_t i = 0; i < count; ++i) {
354 char *name;
355 char name_tmp[12];
356 int_fast32_t x, y, w, h;
357 x = y = w = h = 0;
358
359 name = NULL;
360 if (randr)
361 randr_screen_geo(xcb, i, &x, &y, &w, &h, NULL, &name);
362 else if (xinerama)
363 xinerama_screen_geo(xcb, i, &x, &y, &w, &h);
364 else
365 x11_screen_geo(xcb, i, &w, &h);
366
367 if (name == NULL) {
368 sprintf(name_tmp, "%" PRIuFAST32, i);
369 name = name_tmp;
370 }
371
372 dstr_printf(&screen_info,
373 "Screen %s (%" PRIuFAST32 "x%" PRIuFAST32
374 " @ %" PRIuFAST32 ",%" PRIuFAST32 ")",
375 name, w, h, x, y);
376
377 if (name != name_tmp)
378 free(name);
379
380 if (h > 0 && w > 0)
381 obs_property_list_add_int(screens, screen_info.array,
382 i);
383 }
384
385 /* handle missing screen */
386 if (old_screen + 1 > count) {
387 dstr_printf(&screen_info, "Screen %" PRIuFAST32 " (not found)",
388 old_screen);
389 size_t index = obs_property_list_add_int(
390 screens, screen_info.array, old_screen);
391 obs_property_list_item_disable(screens, index, true);
392 }
393
394 dstr_free(&screen_info);
395
396 xcb_disconnect(xcb);
397 obs_property_set_enabled(screens, true);
398
399 return true;
400 }
401
402 /**
403 * Get the properties for the capture
404 */
xshm_properties(void * vptr)405 static obs_properties_t *xshm_properties(void *vptr)
406 {
407 XSHM_DATA(vptr);
408
409 obs_properties_t *props = obs_properties_create();
410
411 obs_properties_add_list(props, "screen", obs_module_text("Screen"),
412 OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
413 obs_properties_add_bool(props, "show_cursor",
414 obs_module_text("CaptureCursor"));
415 obs_property_t *advanced = obs_properties_add_bool(
416 props, "advanced", obs_module_text("AdvancedSettings"));
417
418 obs_properties_add_int(props, "cut_top", obs_module_text("CropTop"),
419 -4096, 4096, 1);
420 obs_properties_add_int(props, "cut_left", obs_module_text("CropLeft"),
421 -4096, 4096, 1);
422 obs_properties_add_int(props, "cut_right", obs_module_text("CropRight"),
423 0, 4096, 1);
424 obs_properties_add_int(props, "cut_bot", obs_module_text("CropBottom"),
425 0, 4096, 1);
426
427 obs_property_t *server = obs_properties_add_text(
428 props, "server", obs_module_text("XServer"), OBS_TEXT_DEFAULT);
429
430 obs_property_set_modified_callback(advanced, xshm_toggle_advanced);
431 obs_property_set_modified_callback(server, xshm_server_changed);
432
433 /* trigger server callback to get screen count ... */
434 obs_data_t *settings = obs_source_get_settings(data->source);
435 obs_property_modified(server, settings);
436 obs_data_release(settings);
437
438 return props;
439 }
440
441 /**
442 * Destroy the capture
443 */
xshm_destroy(void * vptr)444 static void xshm_destroy(void *vptr)
445 {
446 XSHM_DATA(vptr);
447
448 if (!data)
449 return;
450
451 xshm_capture_stop(data);
452
453 bfree(data);
454 }
455
456 /**
457 * Create the capture
458 */
xshm_create(obs_data_t * settings,obs_source_t * source)459 static void *xshm_create(obs_data_t *settings, obs_source_t *source)
460 {
461 struct xshm_data *data = bzalloc(sizeof(struct xshm_data));
462 data->source = source;
463
464 xshm_update(data, settings);
465
466 return data;
467 }
468
469 /**
470 * Prepare the capture data
471 */
xshm_video_tick(void * vptr,float seconds)472 static void xshm_video_tick(void *vptr, float seconds)
473 {
474 UNUSED_PARAMETER(seconds);
475 XSHM_DATA(vptr);
476
477 if (!data->texture)
478 return;
479 if (!obs_source_showing(data->source))
480 return;
481
482 xcb_shm_get_image_cookie_t img_c;
483 xcb_shm_get_image_reply_t *img_r;
484 xcb_xfixes_get_cursor_image_cookie_t cur_c;
485 xcb_xfixes_get_cursor_image_reply_t *cur_r;
486
487 img_c = xcb_shm_get_image_unchecked(data->xcb, data->xcb_screen->root,
488 data->adj_x_org, data->adj_y_org,
489 data->adj_width, data->adj_height,
490 ~0, XCB_IMAGE_FORMAT_Z_PIXMAP,
491 data->xshm->seg, 0);
492 cur_c = xcb_xfixes_get_cursor_image_unchecked(data->xcb);
493
494 img_r = xcb_shm_get_image_reply(data->xcb, img_c, NULL);
495 cur_r = xcb_xfixes_get_cursor_image_reply(data->xcb, cur_c, NULL);
496
497 if (!img_r)
498 goto exit;
499
500 obs_enter_graphics();
501
502 gs_texture_set_image(data->texture, (void *)data->xshm->data,
503 data->adj_width * 4, false);
504 xcb_xcursor_update(data->cursor, cur_r);
505
506 obs_leave_graphics();
507
508 exit:
509 free(img_r);
510 free(cur_r);
511 }
512
513 /**
514 * Render the capture data
515 */
xshm_video_render(void * vptr,gs_effect_t * effect)516 static void xshm_video_render(void *vptr, gs_effect_t *effect)
517 {
518 XSHM_DATA(vptr);
519
520 effect = obs_get_base_effect(OBS_EFFECT_OPAQUE);
521
522 if (!data->texture)
523 return;
524
525 const bool linear_srgb = gs_get_linear_srgb();
526
527 const bool previous = gs_framebuffer_srgb_enabled();
528 gs_enable_framebuffer_srgb(linear_srgb);
529
530 gs_eparam_t *image = gs_effect_get_param_by_name(effect, "image");
531 if (linear_srgb)
532 gs_effect_set_texture_srgb(image, data->texture);
533 else
534 gs_effect_set_texture(image, data->texture);
535
536 while (gs_effect_loop(effect, "Draw")) {
537 gs_draw_sprite(data->texture, 0, 0, 0);
538 }
539
540 gs_enable_framebuffer_srgb(previous);
541
542 if (data->show_cursor) {
543 effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);
544
545 while (gs_effect_loop(effect, "Draw")) {
546 xcb_xcursor_render(data->cursor);
547 }
548 }
549 }
550
551 /**
552 * Width of the captured data
553 */
xshm_getwidth(void * vptr)554 static uint32_t xshm_getwidth(void *vptr)
555 {
556 XSHM_DATA(vptr);
557 return data->adj_width;
558 }
559
560 /**
561 * Height of the captured data
562 */
xshm_getheight(void * vptr)563 static uint32_t xshm_getheight(void *vptr)
564 {
565 XSHM_DATA(vptr);
566 return data->adj_height;
567 }
568
569 struct obs_source_info xshm_input = {
570 .id = "xshm_input",
571 .type = OBS_SOURCE_TYPE_INPUT,
572 .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW |
573 OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_SRGB,
574 .get_name = xshm_getname,
575 .create = xshm_create,
576 .destroy = xshm_destroy,
577 .update = xshm_update,
578 .get_defaults = xshm_defaults,
579 .get_properties = xshm_properties,
580 .video_tick = xshm_video_tick,
581 .video_render = xshm_video_render,
582 .get_width = xshm_getwidth,
583 .get_height = xshm_getheight,
584 .icon_type = OBS_ICON_TYPE_DESKTOP_CAPTURE,
585 };
586