682 lines
25 KiB
C
682 lines
25 KiB
C
#include <gtk/gtk.h>
|
|
#include <gdk/gdkx.h>
|
|
#include <glib-unix.h>
|
|
#include <webkit2/webkit2.h>
|
|
#include <X11/Xlib.h>
|
|
#include <X11/Xutil.h>
|
|
#include <X11/extensions/XShm.h>
|
|
#include <X11/extensions/Xcomposite.h>
|
|
#include <X11/extensions/Xdamage.h>
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <dirent.h>
|
|
#include <limits.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/ipc.h>
|
|
#include <sys/shm.h>
|
|
#include <unistd.h>
|
|
|
|
#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;
|
|
}
|