diff --git a/CMakeLists.txt b/CMakeLists.txt index 760b599..e27bf4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ pkg_check_modules(OBS REQUIRED IMPORTED_TARGET libobs) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(WEBKIT REQUIRED IMPORTED_TARGET webkit2gtk-4.1) -pkg_check_modules(X11_CAPTURE REQUIRED IMPORTED_TARGET x11 xcomposite xext) +pkg_check_modules(X11_CAPTURE REQUIRED IMPORTED_TARGET x11 xcomposite xdamage xext) add_library(obs-webkit-browser MODULE src/plugin.c diff --git a/README.md b/README.md index e67a922..1e0930d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ WebKitGTK 4.1 в отдельном процессе на приватном Xvf при этом Xvfb используется только как изолированный невидимый дисплей; - прямой XShm-захват итогового кадра Xvfb для непрозрачных страниц и XComposite/XShm-захват с alpha для прозрачных; +- XDamage-пропуск чтения framebuffer, пока содержимое страницы не меняется; - shared-memory транспорт кадров без многомегабайтной записи в pipe; - мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS; - перезагрузка страницы и остановка renderer-процесса у скрытого source. diff --git a/scripts/install-deps-alpine.sh b/scripts/install-deps-alpine.sh index 67048ab..b653c86 100755 --- a/scripts/install-deps-alpine.sh +++ b/scripts/install-deps-alpine.sh @@ -17,5 +17,5 @@ $ROOT apk add \ obs-studio obs-studio-dev \ gtk+3.0 gtk+3.0-dev \ webkit2gtk-4.1 webkit2gtk-4.1-dev \ - libx11-dev libxcomposite-dev libxext-dev \ + libx11-dev libxcomposite-dev libxdamage-dev libxext-dev \ xvfb mesa-dri-gallium diff --git a/src/renderer.c b/src/renderer.c index 2d7a704..82a7c85 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -52,6 +53,8 @@ struct renderer { 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; @@ -199,6 +202,10 @@ 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; /* An opaque WebKit view may render into child/compositor surfaces which are * absent from the top-level window's named backing pixmap. Xvfb's root @@ -224,6 +231,10 @@ static bool initialize_x11_capture(struct renderer *renderer) 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; @@ -257,6 +268,8 @@ static bool initialize_x11_capture(struct renderer *renderer) 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); @@ -288,11 +301,28 @@ static void publish_frame(struct renderer *renderer, const uint8_t *pixels, int 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); - if (!preserve_alpha) { - for (size_t offset = 3, size = owb_frame_size(renderer->config.width, - renderer->config.height); - offset < size; offset += 4) - destination[offset] = 255; + 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; @@ -310,6 +340,18 @@ static void publish_x11_frame(struct renderer *renderer) renderer->config.transparent && renderer->capture_image->depth == 32); } +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; @@ -325,8 +367,17 @@ static void *capture_loop(void *opaque) long interval_ns = 1000000000L / (long)renderer->config.fps; struct timespec deadline; clock_gettime(CLOCK_MONOTONIC, &deadline); + bool first_frame = true; while (!atomic_load_explicit(&renderer->stopping, memory_order_acquire)) { - publish_x11_frame(renderer); + bool damaged = capture_was_damaged(renderer); + if (first_frame || damaged) { + /* Clear the accumulated region before capture. Changes arriving while + * XShmGetImage runs then produce a fresh notification for the next tick. */ + XDamageSubtract(renderer->capture_display, renderer->capture_damage, + None, None); + publish_x11_frame(renderer); + first_frame = false; + } advance_deadline(&deadline, interval_ns); while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &deadline, NULL) == EINTR) { } @@ -548,6 +599,15 @@ int main(void) 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); @@ -600,7 +660,7 @@ int main(void) while (gtk_events_pending()) gtk_main_iteration(); if (!initialize_x11_capture(&renderer)) { - fprintf(stderr, "Cannot initialize XComposite/XShm capture\n"); + fprintf(stderr, "Cannot initialize XComposite/XDamage/XShm capture\n"); return 6; } diff --git a/tests/renderer-smoke-test.c b/tests/renderer-smoke-test.c index 87d5aad..6d7dbba 100644 --- a/tests/renderer-smoke-test.c +++ b/tests/renderer-smoke-test.c @@ -101,9 +101,10 @@ int main(int argc, char **argv) return 2; } - const char internal_url[] = "data:text/html,
"; - const char transparent_css[] = "html,body{margin:0;background:transparent!important}" - "#box{position:fixed;inset:0;background:rgba(255,0,0,.5)}"; + const char internal_url[] = + "data:text/html,
"; + const char transparent_css[] = "html,body{margin:0}" + "#box{position:fixed;inset:16px;background:rgba(255,0,0,.5)}"; const char opaque_css[] = "html,body{margin:0;background:rgb(7,19,31)!important}" "#box{position:fixed;left:16px;top:16px;width:32px;height:32px;" "background:rgb(241,23,17);transform:translateZ(0);filter:blur(.1px)}"; @@ -125,6 +126,7 @@ int main(int argc, char **argv) write_exact(control[0], url, strlen(url)) && write_exact(control[0], css, strlen(css)); uint8_t last_bgra[4] = {0}; + uint8_t last_corner_bgra[4] = {0}; bool found_red = false; int max_attempts = external_url ? 120 : 60; for (int attempt = 0; sent && attempt < max_attempts; attempt++) { @@ -158,11 +160,14 @@ int main(int argc, char **argv) } size_t center = ((size_t)(height / 2) * width + width / 2) * 4; memcpy(last_bgra, frame + center, sizeof(last_bgra)); + memcpy(last_corner_bgra, frame, sizeof(last_corner_bgra)); bool expected = opaque ? last_bgra[0] < 32 && last_bgra[1] < 40 && last_bgra[2] > 210 && last_bgra[3] == 255 - : last_bgra[0] < 8 && last_bgra[1] < 8 && last_bgra[2] > 120 && - last_bgra[2] < 136 && last_bgra[3] > 120 && last_bgra[3] < 136; + : last_bgra[0] < 8 && last_bgra[1] < 8 && last_bgra[2] > 248 && + last_bgra[3] > 120 && last_bgra[3] < 136 && + last_corner_bgra[0] == 0 && last_corner_bgra[1] == 0 && + last_corner_bgra[2] == 0 && last_corner_bgra[3] == 0; if (expected) { found_red = true; break; @@ -183,9 +188,10 @@ int main(int argc, char **argv) } fprintf(stderr, "renderer did not produce the expected %s red BGRA frame " - "(center BGRA=%u,%u,%u,%u)\n", + "(center BGRA=%u,%u,%u,%u; corner BGRA=%u,%u,%u,%u)\n", opaque ? "opaque" : "transparent", last_bgra[0], last_bgra[1], - last_bgra[2], last_bgra[3]); + last_bgra[2], last_bgra[3], last_corner_bgra[0], last_corner_bgra[1], + last_corner_bgra[2], last_corner_bgra[3]); return 1; } return 0;