add transparency
This commit is contained in:
@@ -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(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
pkg_check_modules(WEBKIT REQUIRED IMPORTED_TARGET webkit2gtk-4.1)
|
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
|
add_library(obs-webkit-browser MODULE
|
||||||
src/plugin.c
|
src/plugin.c
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ WebKitGTK 4.1 в отдельном процессе на приватном Xvf
|
|||||||
при этом Xvfb используется только как изолированный невидимый дисплей;
|
при этом Xvfb используется только как изолированный невидимый дисплей;
|
||||||
- прямой XShm-захват итогового кадра Xvfb для непрозрачных страниц и
|
- прямой XShm-захват итогового кадра Xvfb для непрозрачных страниц и
|
||||||
XComposite/XShm-захват с alpha для прозрачных;
|
XComposite/XShm-захват с alpha для прозрачных;
|
||||||
|
- XDamage-пропуск чтения framebuffer, пока содержимое страницы не меняется;
|
||||||
- shared-memory транспорт кадров без многомегабайтной записи в pipe;
|
- shared-memory транспорт кадров без многомегабайтной записи в pipe;
|
||||||
- мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS;
|
- мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS;
|
||||||
- перезагрузка страницы и остановка renderer-процесса у скрытого source.
|
- перезагрузка страницы и остановка renderer-процесса у скрытого source.
|
||||||
|
|||||||
@@ -17,5 +17,5 @@ $ROOT apk add \
|
|||||||
obs-studio obs-studio-dev \
|
obs-studio obs-studio-dev \
|
||||||
gtk+3.0 gtk+3.0-dev \
|
gtk+3.0 gtk+3.0-dev \
|
||||||
webkit2gtk-4.1 webkit2gtk-4.1-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
|
xvfb mesa-dri-gallium
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <X11/Xutil.h>
|
#include <X11/Xutil.h>
|
||||||
#include <X11/extensions/XShm.h>
|
#include <X11/extensions/XShm.h>
|
||||||
#include <X11/extensions/Xcomposite.h>
|
#include <X11/extensions/Xcomposite.h>
|
||||||
|
#include <X11/extensions/Xdamage.h>
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
@@ -52,6 +53,8 @@ struct renderer {
|
|||||||
Display *capture_display;
|
Display *capture_display;
|
||||||
Window capture_window;
|
Window capture_window;
|
||||||
Pixmap capture_pixmap;
|
Pixmap capture_pixmap;
|
||||||
|
Damage capture_damage;
|
||||||
|
int damage_event_base;
|
||||||
XImage *capture_image;
|
XImage *capture_image;
|
||||||
XShmSegmentInfo capture_shm;
|
XShmSegmentInfo capture_shm;
|
||||||
bool capture_redirected;
|
bool capture_redirected;
|
||||||
@@ -199,6 +202,10 @@ static bool initialize_x11_capture(struct renderer *renderer)
|
|||||||
renderer->capture_display = XOpenDisplay(NULL);
|
renderer->capture_display = XOpenDisplay(NULL);
|
||||||
if (!renderer->capture_display || !XShmQueryExtension(renderer->capture_display))
|
if (!renderer->capture_display || !XShmQueryExtension(renderer->capture_display))
|
||||||
return false;
|
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
|
/* 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
|
* 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)
|
if (!renderer->capture_pixmap)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
renderer->capture_damage = XDamageCreate(renderer->capture_display,
|
||||||
|
renderer->capture_window, XDamageReportNonEmpty);
|
||||||
|
if (!renderer->capture_damage)
|
||||||
|
return false;
|
||||||
XWindowAttributes attributes;
|
XWindowAttributes attributes;
|
||||||
if (!XGetWindowAttributes(renderer->capture_display, renderer->capture_window, &attributes))
|
if (!XGetWindowAttributes(renderer->capture_display, renderer->capture_window, &attributes))
|
||||||
return false;
|
return false;
|
||||||
@@ -257,6 +268,8 @@ static bool initialize_x11_capture(struct renderer *renderer)
|
|||||||
|
|
||||||
static void destroy_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) {
|
if (renderer->capture_attached) {
|
||||||
XShmDetach(renderer->capture_display, &renderer->capture_shm);
|
XShmDetach(renderer->capture_display, &renderer->capture_shm);
|
||||||
XSync(renderer->capture_display, False);
|
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++)
|
for (uint32_t y = 0; y < renderer->config.height; y++)
|
||||||
memcpy(destination + (size_t)y * renderer->config.width * 4,
|
memcpy(destination + (size_t)y * renderer->config.width * 4,
|
||||||
pixels + (size_t)y * (size_t)stride, (size_t)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) {
|
if (!preserve_alpha) {
|
||||||
for (size_t offset = 3, size = owb_frame_size(renderer->config.width,
|
pixel[3] = 255;
|
||||||
renderer->config.height);
|
continue;
|
||||||
offset < size; offset += 4)
|
}
|
||||||
destination[offset] = 255;
|
|
||||||
|
/* 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);
|
__atomic_store_n(&renderer->shared->sequence[slot], complete_sequence, __ATOMIC_RELEASE);
|
||||||
uint8_t notification = (uint8_t)slot;
|
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);
|
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)
|
static void advance_deadline(struct timespec *deadline, long interval_ns)
|
||||||
{
|
{
|
||||||
deadline->tv_nsec += 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;
|
long interval_ns = 1000000000L / (long)renderer->config.fps;
|
||||||
struct timespec deadline;
|
struct timespec deadline;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
||||||
|
bool first_frame = true;
|
||||||
while (!atomic_load_explicit(&renderer->stopping, memory_order_acquire)) {
|
while (!atomic_load_explicit(&renderer->stopping, memory_order_acquire)) {
|
||||||
|
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);
|
publish_x11_frame(renderer);
|
||||||
|
first_frame = false;
|
||||||
|
}
|
||||||
advance_deadline(&deadline, interval_ns);
|
advance_deadline(&deadline, interval_ns);
|
||||||
while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &deadline, NULL) == EINTR) {
|
while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &deadline, NULL) == EINTR) {
|
||||||
}
|
}
|
||||||
@@ -548,6 +599,15 @@ int main(void)
|
|||||||
NULL, NULL);
|
NULL, NULL);
|
||||||
webkit_user_content_manager_add_script(manager, reporter);
|
webkit_user_content_manager_add_script(manager, reporter);
|
||||||
webkit_user_script_unref(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) {
|
if (*renderer.config.css) {
|
||||||
WebKitUserStyleSheet *sheet = webkit_user_style_sheet_new(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_INJECT_ALL_FRAMES, WEBKIT_USER_STYLE_LEVEL_USER, NULL, NULL);
|
||||||
@@ -600,7 +660,7 @@ int main(void)
|
|||||||
while (gtk_events_pending())
|
while (gtk_events_pending())
|
||||||
gtk_main_iteration();
|
gtk_main_iteration();
|
||||||
if (!initialize_x11_capture(&renderer)) {
|
if (!initialize_x11_capture(&renderer)) {
|
||||||
fprintf(stderr, "Cannot initialize XComposite/XShm capture\n");
|
fprintf(stderr, "Cannot initialize XComposite/XDamage/XShm capture\n");
|
||||||
return 6;
|
return 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,9 +101,10 @@ int main(int argc, char **argv)
|
|||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char internal_url[] = "data:text/html,<body><div id='box'></div></body>";
|
const char internal_url[] =
|
||||||
const char transparent_css[] = "html,body{margin:0;background:transparent!important}"
|
"data:text/html,<body style='background:rgb(3,5,7)'><div id='box'></div></body>";
|
||||||
"#box{position:fixed;inset:0;background:rgba(255,0,0,.5)}";
|
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}"
|
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;"
|
"#box{position:fixed;left:16px;top:16px;width:32px;height:32px;"
|
||||||
"background:rgb(241,23,17);transform:translateZ(0);filter:blur(.1px)}";
|
"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], url, strlen(url)) &&
|
||||||
write_exact(control[0], css, strlen(css));
|
write_exact(control[0], css, strlen(css));
|
||||||
uint8_t last_bgra[4] = {0};
|
uint8_t last_bgra[4] = {0};
|
||||||
|
uint8_t last_corner_bgra[4] = {0};
|
||||||
bool found_red = false;
|
bool found_red = false;
|
||||||
int max_attempts = external_url ? 120 : 60;
|
int max_attempts = external_url ? 120 : 60;
|
||||||
for (int attempt = 0; sent && attempt < max_attempts; attempt++) {
|
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;
|
size_t center = ((size_t)(height / 2) * width + width / 2) * 4;
|
||||||
memcpy(last_bgra, frame + center, sizeof(last_bgra));
|
memcpy(last_bgra, frame + center, sizeof(last_bgra));
|
||||||
|
memcpy(last_corner_bgra, frame, sizeof(last_corner_bgra));
|
||||||
bool expected = opaque
|
bool expected = opaque
|
||||||
? last_bgra[0] < 32 && last_bgra[1] < 40 && last_bgra[2] > 210 &&
|
? last_bgra[0] < 32 && last_bgra[1] < 40 && last_bgra[2] > 210 &&
|
||||||
last_bgra[3] == 255
|
last_bgra[3] == 255
|
||||||
: last_bgra[0] < 8 && last_bgra[1] < 8 && last_bgra[2] > 120 &&
|
: last_bgra[0] < 8 && last_bgra[1] < 8 && last_bgra[2] > 248 &&
|
||||||
last_bgra[2] < 136 && last_bgra[3] > 120 && last_bgra[3] < 136;
|
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) {
|
if (expected) {
|
||||||
found_red = true;
|
found_red = true;
|
||||||
break;
|
break;
|
||||||
@@ -183,9 +188,10 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"renderer did not produce the expected %s red BGRA frame "
|
"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],
|
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 1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user