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