#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "protocol.h" struct config { uint32_t width; uint32_t height; uint32_t fps; bool transparent; char *url; char *css; }; struct renderer { struct config config; GtkWidget *window; WebKitWebView *web_view; struct owb_shared_frames *shared; size_t shared_size; uint64_t next_sequence; atomic_bool stopping; pthread_t capture_thread; bool capture_thread_started; uint8_t command_buffer[sizeof(struct owb_command)]; size_t command_used; Display *capture_display; Window capture_window; Pixmap capture_pixmap; Damage capture_damage; int damage_event_base; XImage *capture_image; XShmSegmentInfo capture_shm; bool capture_redirected; bool capture_attached; }; static bool read_exact(int fd, void *data, size_t size) { uint8_t *cursor = data; while (size > 0) { ssize_t count = read(fd, cursor, size); if (count < 0 && errno == EINTR) continue; if (count <= 0) return false; cursor += (size_t)count; size -= (size_t)count; } return true; } static bool read_config(struct config *config) { struct owb_config_header header; if (!read_exact(STDIN_FILENO, &header, sizeof(header)) || header.magic != OWB_CONFIG_MAGIC || header.version != OWB_PROTOCOL_VERSION || header.width < 2 || header.height < 2 || header.width > 8192 || header.height > 8192 || header.fps < 1 || header.fps > 60 || header.url_length > OWB_MAX_STRING || header.css_length > OWB_MAX_STRING) return false; config->width = header.width; config->height = header.height; config->fps = header.fps; config->transparent = header.transparent != 0; /* Protocol v2 retains this legacy flag, but this renderer always enables the * WebKit compositor. */ (void)header.hardware_acceleration; config->url = calloc((size_t)header.url_length + 1, 1); config->css = calloc((size_t)header.css_length + 1, 1); if (!config->url || !config->css) return false; return read_exact(STDIN_FILENO, config->url, header.url_length) && read_exact(STDIN_FILENO, config->css, header.css_length); } static char *normalize_uri(const char *value) { char *trimmed = g_strdup(value); g_strstrip(trimmed); if (strstr(trimmed, "://") || g_str_has_prefix(trimmed, "data:") || g_str_has_prefix(trimmed, "about:")) return trimmed; if (g_file_test(trimmed, G_FILE_TEST_EXISTS)) { char *absolute = g_canonicalize_filename(trimmed, NULL); char *uri = g_filename_to_uri(absolute, NULL, NULL); g_free(absolute); g_free(trimmed); return uri; } char *uri = g_strdup_printf("https://%s", trimmed); g_free(trimmed); return uri; } static void prepare_webkit(void) { /* Reuse OBS' X11 or Wayland connection. WebKitGTK's DMA-BUF backing store * needs a native GdkWindow to create the GL context used to import GPU * buffers. The window is made transparent and non-interactive below. */ g_setenv("GDK_SCALE", "1", TRUE); g_setenv("GDK_DPI_SCALE", "1", TRUE); g_setenv("GDK_BACKEND", "x11", TRUE); g_unsetenv("LIBGL_ALWAYS_SOFTWARE"); g_unsetenv("GALLIUM_DRIVER"); g_unsetenv("MESA_LOADER_DRIVER_OVERRIDE"); g_unsetenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM"); g_unsetenv("WEBKIT_DMABUF_RENDERER_DISABLE_GBM"); g_setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "0", TRUE); g_setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "0", TRUE); g_setenv("WEBKIT_FORCE_COMPOSITING_MODE", "1", TRUE); g_setenv("NO_AT_BRIDGE", "1", TRUE); } static bool process_uses_drm_render_node(void) { DIR *directory = opendir("/proc/self/fd"); if (!directory) return false; bool found = false; struct dirent *entry; while (!found && (entry = readdir(directory))) { if (entry->d_name[0] == '.') continue; char descriptor[64]; char target[PATH_MAX]; g_snprintf(descriptor, sizeof(descriptor), "/proc/self/fd/%s", entry->d_name); ssize_t length = readlink(descriptor, target, sizeof(target) - 1); if (length <= 0) continue; target[length] = '\0'; found = g_str_has_prefix(target, "/dev/dri/renderD"); } closedir(directory); return found; } static bool map_shared_frames(struct renderer *renderer) { struct stat info; renderer->shared_size = owb_shared_size(renderer->config.width, renderer->config.height); if (fstat(OWB_SHARED_FRAME_FD, &info) != 0 || (size_t)info.st_size < renderer->shared_size) return false; renderer->shared = mmap(NULL, renderer->shared_size, PROT_READ | PROT_WRITE, MAP_SHARED, OWB_SHARED_FRAME_FD, 0); if (renderer->shared == MAP_FAILED) return false; return renderer->shared->magic == OWB_SHARED_MAGIC && renderer->shared->version == OWB_PROTOCOL_VERSION && renderer->shared->width == renderer->config.width && renderer->shared->height == renderer->config.height && renderer->shared->stride == renderer->config.width * 4 && renderer->shared->slots == OWB_SHARED_FRAME_SLOTS; } static bool initialize_x11_capture(struct renderer *renderer) { renderer->capture_display = XOpenDisplay(NULL); if (!renderer->capture_display || !XShmQueryExtension(renderer->capture_display)) return false; int damage_error_base = 0; if (!XDamageQueryExtension(renderer->capture_display, &renderer->damage_event_base, &damage_error_base)) return false; int composite_event_base = 0; int composite_error_base = 0; if (!XCompositeQueryExtension(renderer->capture_display, &composite_event_base, &composite_error_base)) return false; GdkWindow *gdk_window = gtk_widget_get_window(renderer->window); if (!gdk_window || !GDK_IS_X11_WINDOW(gdk_window)) return false; renderer->capture_window = GDK_WINDOW_XID(gdk_window); XCompositeRedirectWindow(renderer->capture_display, renderer->capture_window, CompositeRedirectManual); renderer->capture_redirected = true; XSync(renderer->capture_display, False); renderer->capture_pixmap = XCompositeNameWindowPixmap(renderer->capture_display, renderer->capture_window); if (!renderer->capture_pixmap) return false; renderer->capture_damage = XDamageCreate(renderer->capture_display, renderer->capture_window, XDamageReportNonEmpty); if (!renderer->capture_damage) return false; XWindowAttributes attributes; if (!XGetWindowAttributes(renderer->capture_display, renderer->capture_window, &attributes)) return false; memset(&renderer->capture_shm, 0, sizeof(renderer->capture_shm)); renderer->capture_shm.shmid = -1; renderer->capture_image = XShmCreateImage(renderer->capture_display, attributes.visual, (unsigned)attributes.depth, ZPixmap, NULL, &renderer->capture_shm, renderer->config.width, renderer->config.height); if (!renderer->capture_image || renderer->capture_image->bits_per_pixel != 32) return false; size_t image_size = (size_t)renderer->capture_image->bytes_per_line * (size_t)renderer->capture_image->height; renderer->capture_shm.shmid = shmget(IPC_PRIVATE, image_size, IPC_CREAT | 0600); if (renderer->capture_shm.shmid < 0) return false; renderer->capture_shm.shmaddr = shmat(renderer->capture_shm.shmid, NULL, 0); if (renderer->capture_shm.shmaddr == (char *)-1) { renderer->capture_shm.shmaddr = NULL; return false; } renderer->capture_image->data = renderer->capture_shm.shmaddr; renderer->capture_shm.readOnly = False; if (!XShmAttach(renderer->capture_display, &renderer->capture_shm)) return false; renderer->capture_attached = true; XSync(renderer->capture_display, False); shmctl(renderer->capture_shm.shmid, IPC_RMID, NULL); return true; } static void destroy_x11_capture(struct renderer *renderer) { if (renderer->capture_damage) XDamageDestroy(renderer->capture_display, renderer->capture_damage); if (renderer->capture_attached) { XShmDetach(renderer->capture_display, &renderer->capture_shm); XSync(renderer->capture_display, False); } if (renderer->capture_image) { renderer->capture_image->data = NULL; XDestroyImage(renderer->capture_image); } if (renderer->capture_shm.shmaddr) shmdt(renderer->capture_shm.shmaddr); if (renderer->capture_shm.shmid >= 0 && !renderer->capture_attached) shmctl(renderer->capture_shm.shmid, IPC_RMID, NULL); if (renderer->capture_pixmap) XFreePixmap(renderer->capture_display, renderer->capture_pixmap); if (renderer->capture_redirected) XCompositeUnredirectWindow(renderer->capture_display, renderer->capture_window, CompositeRedirectManual); if (renderer->capture_display) XCloseDisplay(renderer->capture_display); } static void publish_frame(struct renderer *renderer, const uint8_t *pixels, int stride, bool preserve_alpha) { uint32_t slot = (uint32_t)(renderer->next_sequence % OWB_SHARED_FRAME_SLOTS); uint64_t complete_sequence = (++renderer->next_sequence) << 1; __atomic_store_n(&renderer->shared->sequence[slot], complete_sequence | 1u, __ATOMIC_RELEASE); uint8_t *destination = owb_shared_slot(renderer->shared, slot); for (uint32_t y = 0; y < renderer->config.height; y++) memcpy(destination + (size_t)y * renderer->config.width * 4, pixels + (size_t)y * (size_t)stride, (size_t)renderer->config.width * 4); size_t size = owb_frame_size(renderer->config.width, renderer->config.height); for (size_t offset = 0; offset < size; offset += 4) { uint8_t *pixel = destination + offset; if (!preserve_alpha) { pixel[3] = 255; continue; } /* XComposite ARGB pixmaps contain premultiplied color channels, while * OBS treats an asynchronous VIDEO_FORMAT_BGRA frame as straight alpha. * Passing the pixmap through unchanged multiplies translucent colors a * second time during scene compositing and leaves black fringes. */ unsigned alpha = pixel[3]; if (alpha == 0) { pixel[0] = pixel[1] = pixel[2] = 0; } else if (alpha < 255) { for (size_t channel = 0; channel < 3; channel++) { unsigned straight = ((unsigned)pixel[channel] * 255u + alpha / 2u) / alpha; pixel[channel] = (uint8_t)(straight > 255u ? 255u : straight); } } } __atomic_store_n(&renderer->shared->sequence[slot], complete_sequence, __ATOMIC_RELEASE); uint8_t notification = (uint8_t)slot; ssize_t ignored = write(STDOUT_FILENO, ¬ification, sizeof(notification)); (void)ignored; } static bool publish_x11_frame(struct renderer *renderer) { if (!XShmGetImage(renderer->capture_display, renderer->capture_pixmap, renderer->capture_image, 0, 0, AllPlanes)) return false; publish_frame(renderer, (const uint8_t *)renderer->capture_image->data, renderer->capture_image->bytes_per_line, renderer->config.transparent && renderer->capture_image->depth == 32); return true; } static bool capture_was_damaged(struct renderer *renderer) { bool damaged = false; while (XPending(renderer->capture_display)) { XEvent event; XNextEvent(renderer->capture_display, &event); if (event.type == renderer->damage_event_base + XDamageNotify) damaged = true; } return damaged; } static void advance_deadline(struct timespec *deadline, long interval_ns) { deadline->tv_nsec += interval_ns; while (deadline->tv_nsec >= 1000000000L) { deadline->tv_nsec -= 1000000000L; deadline->tv_sec++; } } static void *capture_loop(void *opaque) { struct renderer *renderer = opaque; long interval_ns = 1000000000L / (long)renderer->config.fps; struct timespec deadline; clock_gettime(CLOCK_MONOTONIC, &deadline); bool first_frame = true; bool transport_reported = false; while (!atomic_load_explicit(&renderer->stopping, memory_order_acquire)) { bool damaged = capture_was_damaged(renderer); if (first_frame || damaged) { /* Clear before capture so updates arriving during XShmGetImage * produce a fresh notification for the next tick. */ XDamageSubtract(renderer->capture_display, renderer->capture_damage, None, None); bool published = publish_x11_frame(renderer); if (published && !transport_reported && process_uses_drm_render_node()) { fprintf(stderr, "DRM render node: active\n"); transport_reported = true; } first_frame = false; } advance_deadline(&deadline, interval_ns); while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &deadline, NULL) == EINTR) { } struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); if (now.tv_sec > deadline.tv_sec + 1) deadline = now; } return NULL; } static GdkModifierType modifiers_from_obs(uint32_t modifiers) { GdkModifierType state = 0; if (modifiers & (1u << 1)) state |= GDK_SHIFT_MASK; if (modifiers & (1u << 2)) state |= GDK_CONTROL_MASK; if (modifiers & (1u << 3)) state |= GDK_MOD1_MASK; if (modifiers & (1u << 4)) state |= GDK_BUTTON1_MASK; if (modifiers & (1u << 5)) state |= GDK_BUTTON2_MASK; if (modifiers & (1u << 6)) state |= GDK_BUTTON3_MASK; return state; } static void dispatch_command(struct renderer *renderer, const struct owb_command *command) { if (command->magic != OWB_COMMAND_MAGIC) return; GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(renderer->web_view)); if (!window) return; if (command->type == OWB_RELOAD) { webkit_web_view_reload(renderer->web_view); return; } if (command->type == OWB_FOCUS) { gtk_window_set_focus(GTK_WINDOW(renderer->window), command->value1 ? GTK_WIDGET(renderer->web_view) : NULL); return; } if (command->type == OWB_MOUSE_MOVE) { if (command->value1) { GdkEvent *event = gdk_event_new(GDK_LEAVE_NOTIFY); event->crossing.window = g_object_ref(window); event->crossing.send_event = TRUE; event->crossing.time = GDK_CURRENT_TIME; event->crossing.x = command->x; event->crossing.y = command->y; event->crossing.mode = GDK_CROSSING_NORMAL; event->crossing.detail = GDK_NOTIFY_NONLINEAR; event->crossing.state = modifiers_from_obs(command->modifiers); gtk_widget_event(GTK_WIDGET(renderer->web_view), event); gdk_event_free(event); return; } GdkEvent *event = gdk_event_new(GDK_MOTION_NOTIFY); event->motion.window = g_object_ref(window); event->motion.send_event = TRUE; event->motion.time = GDK_CURRENT_TIME; event->motion.x = command->x; event->motion.y = command->y; event->motion.state = modifiers_from_obs(command->modifiers); gtk_widget_event(GTK_WIDGET(renderer->web_view), event); gdk_event_free(event); return; } if (command->type == OWB_MOUSE_CLICK) { GdkEventType type = command->value2 ? GDK_BUTTON_RELEASE : command->text_length == 2 ? GDK_2BUTTON_PRESS : command->text_length >= 3 ? GDK_3BUTTON_PRESS : GDK_BUTTON_PRESS; GdkEvent *event = gdk_event_new(type); event->button.window = g_object_ref(window); event->button.send_event = TRUE; event->button.time = GDK_CURRENT_TIME; event->button.x = command->x; event->button.y = command->y; event->button.button = command->value1 == 1 ? 2 : command->value1 == 2 ? 3 : 1; event->button.state = modifiers_from_obs(command->modifiers); gtk_widget_event(GTK_WIDGET(renderer->web_view), event); gdk_event_free(event); return; } if (command->type == OWB_MOUSE_WHEEL) { GdkEvent *event = gdk_event_new(GDK_SCROLL); event->scroll.window = g_object_ref(window); event->scroll.send_event = TRUE; event->scroll.time = GDK_CURRENT_TIME; event->scroll.x = command->x; event->scroll.y = command->y; event->scroll.direction = GDK_SCROLL_SMOOTH; event->scroll.delta_x = -(double)command->value1 / 120.0; event->scroll.delta_y = -(double)command->value2 / 120.0; event->scroll.state = modifiers_from_obs(command->modifiers); gtk_widget_event(GTK_WIDGET(renderer->web_view), event); gdk_event_free(event); return; } if (command->type == OWB_KEY) { GdkEvent *event = gdk_event_new(command->value1 ? GDK_KEY_RELEASE : GDK_KEY_PRESS); event->key.window = g_object_ref(window); event->key.send_event = TRUE; event->key.time = GDK_CURRENT_TIME; event->key.state = modifiers_from_obs(command->modifiers); event->key.keyval = (guint)command->value2; if (command->text_length) { gunichar character = g_utf8_get_char_validated(command->text, command->text_length); if (character != (gunichar)-1 && character != (gunichar)-2) event->key.keyval = gdk_unicode_to_keyval(character); } event->key.length = (gint)command->text_length; event->key.string = g_strndup(command->text, command->text_length); gtk_widget_event(GTK_WIDGET(renderer->web_view), event); gdk_event_free(event); } } static gboolean control_ready(gint fd, GIOCondition condition, gpointer opaque) { struct renderer *renderer = opaque; if (condition & (G_IO_HUP | G_IO_ERR)) { gtk_main_quit(); return G_SOURCE_REMOVE; } for (;;) { ssize_t count = read(fd, renderer->command_buffer + renderer->command_used, sizeof(renderer->command_buffer) - renderer->command_used); if (count < 0 && errno == EINTR) continue; if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return G_SOURCE_CONTINUE; if (count <= 0) { gtk_main_quit(); return G_SOURCE_REMOVE; } renderer->command_used += (size_t)count; if (renderer->command_used == sizeof(struct owb_command)) { dispatch_command(renderer, (const struct owb_command *)renderer->command_buffer); renderer->command_used = 0; } } } static gboolean clear_window(GtkWidget *widget, cairo_t *context, gpointer unused) { (void)widget; (void)unused; cairo_set_operator(context, CAIRO_OPERATOR_SOURCE); cairo_set_source_rgba(context, 0, 0, 0, 0); cairo_paint(context); return FALSE; } static gboolean terminate_renderer(gpointer opaque) { struct renderer *renderer = opaque; atomic_store_explicit(&renderer->stopping, true, memory_order_release); gtk_main_quit(); return G_SOURCE_REMOVE; } static void script_message_received(WebKitUserContentManager *manager, WebKitJavascriptResult *message, gpointer unused) { (void)manager; (void)unused; JSCValue *value = webkit_javascript_result_get_js_value(message); char *text = jsc_value_to_string(value); fprintf(stderr, "Page: %s\n", text ? text : "(empty message)"); g_free(text); } static gboolean load_failed(WebKitWebView *view, WebKitLoadEvent event, const char *uri, GError *error, gpointer unused) { (void)view; (void)event; (void)unused; fprintf(stderr, "Page load failed for %s: %s\n", uri, error->message); return FALSE; } int main(void) { struct renderer renderer = {0}; atomic_init(&renderer.stopping, false); if (!read_config(&renderer.config)) { fprintf(stderr, "Invalid renderer configuration\n"); return 2; } prepare_webkit(); if (!map_shared_frames(&renderer)) { fprintf(stderr, "Cannot map shared frame transport\n"); return 4; } if (!gtk_init_check(NULL, NULL)) { fprintf(stderr, "GTK could not connect to the OBS display\n"); return 5; } WebKitUserContentManager *manager = webkit_user_content_manager_new(); g_signal_connect(manager, "script-message-received::owbLog", G_CALLBACK(script_message_received), NULL); if (!webkit_user_content_manager_register_script_message_handler(manager, "owbLog")) fprintf(stderr, "Cannot register page diagnostic message handler\n"); static const char error_reporter[] = "(()=>{const p=(k,v)=>{try{window.webkit.messageHandlers.owbLog.postMessage(k+': '+v)}catch{}};" "addEventListener('error',e=>p('error',e.message+' @ '+e.filename+':'+e.lineno));" "addEventListener('unhandledrejection',e=>p('rejection',e.reason?.stack||e.reason));" "const ce=console.error;console.error=(...a)=>{p('console',a.join(' '));ce(...a)}})();"; WebKitUserScript *reporter = webkit_user_script_new(error_reporter, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, NULL, NULL); webkit_user_content_manager_add_script(manager, reporter); webkit_user_script_unref(reporter); if (renderer.config.transparent) { static const char transparent_page_css[] = "html,body{background:transparent!important}"; WebKitUserStyleSheet *transparent_sheet = webkit_user_style_sheet_new( transparent_page_css, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_STYLE_LEVEL_USER, NULL, NULL); webkit_user_content_manager_add_style_sheet(manager, transparent_sheet); webkit_user_style_sheet_unref(transparent_sheet); } if (*renderer.config.css) { WebKitUserStyleSheet *sheet = webkit_user_style_sheet_new(renderer.config.css, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_STYLE_LEVEL_USER, NULL, NULL); webkit_user_content_manager_add_style_sheet(manager, sheet); webkit_user_style_sheet_unref(sheet); } renderer.web_view = WEBKIT_WEB_VIEW(webkit_web_view_new_with_user_content_manager(manager)); g_signal_connect(renderer.web_view, "load-failed", G_CALLBACK(load_failed), NULL); g_object_unref(manager); WebKitSettings *settings = webkit_web_view_get_settings(renderer.web_view); webkit_settings_set_enable_webgl(settings, TRUE); webkit_settings_set_enable_page_cache(settings, TRUE); webkit_settings_set_enable_smooth_scrolling(settings, TRUE); webkit_settings_set_media_playback_requires_user_gesture(settings, FALSE); webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS); webkit_web_view_set_is_muted(renderer.web_view, TRUE); GdkRGBA background = renderer.config.transparent ? (GdkRGBA){0, 0, 0, 0} : (GdkRGBA){1, 1, 1, 1}; webkit_web_view_set_background_color(renderer.web_view, &background); renderer.window = gtk_window_new(GTK_WINDOW_POPUP); GdkScreen *screen = gdk_screen_get_default(); GdkVisual *visual = renderer.config.transparent ? gdk_screen_get_rgba_visual(screen) : gdk_screen_get_system_visual(screen); if (visual) gtk_widget_set_visual(renderer.window, visual); gtk_widget_set_app_paintable(renderer.window, renderer.config.transparent); if (renderer.config.transparent) g_signal_connect(renderer.window, "draw", G_CALLBACK(clear_window), NULL); gtk_window_set_decorated(GTK_WINDOW(renderer.window), FALSE); gtk_window_set_resizable(GTK_WINDOW(renderer.window), FALSE); gtk_window_set_skip_taskbar_hint(GTK_WINDOW(renderer.window), TRUE); gtk_window_set_skip_pager_hint(GTK_WINDOW(renderer.window), TRUE); gtk_window_set_accept_focus(GTK_WINDOW(renderer.window), FALSE); gtk_window_set_focus_on_map(GTK_WINDOW(renderer.window), FALSE); gtk_window_move(GTK_WINDOW(renderer.window), 0, 0); gtk_window_set_default_size(GTK_WINDOW(renderer.window), (int)renderer.config.width, (int)renderer.config.height); gtk_widget_set_size_request(GTK_WIDGET(renderer.web_view), (int)renderer.config.width, (int)renderer.config.height); gtk_container_add(GTK_CONTAINER(renderer.window), GTK_WIDGET(renderer.web_view)); gtk_widget_realize(renderer.window); GdkWindow *renderer_window = gtk_widget_get_window(renderer.window); if (!renderer_window || gdk_window_get_window_type(renderer_window) == GDK_WINDOW_OFFSCREEN) { fprintf(stderr, "GTK did not create the native window required for GPU rendering\n"); return 6; } cairo_region_t *empty_input = cairo_region_create(); gdk_window_input_shape_combine_region(renderer_window, empty_input, 0, 0); cairo_region_destroy(empty_input); gdk_window_set_opacity(renderer_window, 0.0); gtk_widget_show_all(renderer.window); while (gtk_events_pending()) gtk_main_iteration(); if (!initialize_x11_capture(&renderer)) { fprintf(stderr, "Cannot initialize XComposite/XDamage/XShm GPU capture\n"); return 7; } int output_flags = fcntl(STDOUT_FILENO, F_GETFL, 0); if (output_flags >= 0) fcntl(STDOUT_FILENO, F_SETFL, output_flags | O_NONBLOCK); int flags = fcntl(STDIN_FILENO, F_GETFL, 0); if (flags >= 0) fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); g_unix_fd_add(STDIN_FILENO, G_IO_IN | G_IO_HUP | G_IO_ERR, control_ready, &renderer); char *uri = normalize_uri(renderer.config.url); webkit_web_view_load_uri(renderer.web_view, uri); g_free(uri); if (pthread_create(&renderer.capture_thread, NULL, capture_loop, &renderer) != 0) { fprintf(stderr, "Cannot start GPU capture thread\n"); return 8; } renderer.capture_thread_started = true; fprintf(stderr, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, GPU DMA-BUF, XComposite/XShm capture\n", webkit_get_major_version(), webkit_get_minor_version(), webkit_get_micro_version(), renderer.config.width, renderer.config.height, renderer.config.fps); fflush(stderr); g_unix_signal_add(SIGTERM, terminate_renderer, &renderer); g_unix_signal_add(SIGINT, terminate_renderer, &renderer); gtk_main(); atomic_store_explicit(&renderer.stopping, true, memory_order_release); if (renderer.capture_thread_started) pthread_join(renderer.capture_thread, NULL); destroy_x11_capture(&renderer); munmap(renderer.shared, renderer.shared_size); free(renderer.config.url); free(renderer.config.css); return 0; }