gpu accelerated
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/build/
|
||||
*.o
|
||||
*.so
|
||||
*.swp
|
||||
86
CMakeLists.txt
Normal file
86
CMakeLists.txt
Normal file
@@ -0,0 +1,86 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
project(obs-webkit-browser VERSION 0.1.0 LANGUAGES C)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
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)
|
||||
|
||||
add_library(obs-webkit-browser MODULE
|
||||
src/plugin.c
|
||||
src/protocol.h
|
||||
)
|
||||
target_compile_features(obs-webkit-browser PRIVATE c_std_11)
|
||||
target_compile_definitions(obs-webkit-browser PRIVATE _GNU_SOURCE)
|
||||
target_compile_options(obs-webkit-browser PRIVATE -Wall -Wextra -Wpedantic)
|
||||
target_link_libraries(obs-webkit-browser PRIVATE PkgConfig::OBS Threads::Threads)
|
||||
set_target_properties(obs-webkit-browser PROPERTIES PREFIX "")
|
||||
|
||||
add_executable(obs-webkit-renderer
|
||||
src/renderer.c
|
||||
src/protocol.h
|
||||
)
|
||||
target_compile_features(obs-webkit-renderer PRIVATE c_std_11)
|
||||
target_compile_definitions(obs-webkit-renderer PRIVATE _GNU_SOURCE)
|
||||
target_compile_options(obs-webkit-renderer PRIVATE -Wall -Wextra -Wpedantic)
|
||||
target_link_libraries(obs-webkit-renderer PRIVATE
|
||||
PkgConfig::GLIB
|
||||
PkgConfig::GTK
|
||||
PkgConfig::WEBKIT
|
||||
PkgConfig::X11_CAPTURE
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
set(OBS_PLUGIN_DIR "${CMAKE_INSTALL_LIBDIR}/obs-plugins" CACHE PATH
|
||||
"Directory for the OBS plugin module")
|
||||
set(OBS_PLUGIN_DATA_DIR "${CMAKE_INSTALL_DATADIR}/obs/obs-plugins/obs-webkit-browser"
|
||||
CACHE PATH "Directory for plugin data and the renderer helper")
|
||||
|
||||
install(TARGETS obs-webkit-browser
|
||||
LIBRARY DESTINATION "${OBS_PLUGIN_DIR}")
|
||||
install(TARGETS obs-webkit-renderer
|
||||
RUNTIME DESTINATION "${OBS_PLUGIN_DATA_DIR}")
|
||||
install(FILES data/locale/en-US.ini data/locale/ru-RU.ini
|
||||
DESTINATION "${OBS_PLUGIN_DATA_DIR}/locale")
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_executable(protocol-test tests/protocol-test.c)
|
||||
target_compile_features(protocol-test PRIVATE c_std_11)
|
||||
target_include_directories(protocol-test PRIVATE src)
|
||||
add_test(NAME protocol-test COMMAND protocol-test)
|
||||
|
||||
add_executable(renderer-smoke-test tests/renderer-smoke-test.c)
|
||||
target_compile_features(renderer-smoke-test PRIVATE c_std_11)
|
||||
target_compile_definitions(renderer-smoke-test PRIVATE _GNU_SOURCE)
|
||||
target_include_directories(renderer-smoke-test PRIVATE src)
|
||||
add_test(NAME renderer-smoke-test
|
||||
COMMAND renderer-smoke-test $<TARGET_FILE:obs-webkit-renderer>)
|
||||
set_tests_properties(renderer-smoke-test PROPERTIES TIMEOUT 20)
|
||||
|
||||
add_test(NAME renderer-opaque-smoke-test
|
||||
COMMAND renderer-smoke-test $<TARGET_FILE:obs-webkit-renderer> opaque)
|
||||
set_tests_properties(renderer-opaque-smoke-test PROPERTIES TIMEOUT 20)
|
||||
|
||||
add_executable(renderer-fps-test tests/renderer-fps-test.c)
|
||||
target_compile_features(renderer-fps-test PRIVATE c_std_11)
|
||||
target_compile_definitions(renderer-fps-test PRIVATE _GNU_SOURCE)
|
||||
target_include_directories(renderer-fps-test PRIVATE src)
|
||||
add_test(NAME blur-transition-fps-test
|
||||
COMMAND renderer-fps-test $<TARGET_FILE:obs-webkit-renderer>)
|
||||
set_tests_properties(blur-transition-fps-test PROPERTIES TIMEOUT 20)
|
||||
|
||||
add_executable(module-load-test tests/module-load-test.c)
|
||||
target_compile_features(module-load-test PRIVATE c_std_11)
|
||||
target_link_libraries(module-load-test PRIVATE PkgConfig::OBS)
|
||||
add_test(NAME module-load-test COMMAND module-load-test
|
||||
$<TARGET_FILE:obs-webkit-browser> "${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||
$<TARGET_FILE:obs-webkit-renderer>)
|
||||
set_tests_properties(module-load-test PROPERTIES TIMEOUT 20)
|
||||
endif()
|
||||
15
LICENSE
Normal file
15
LICENSE
Normal file
@@ -0,0 +1,15 @@
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
Copyright (C) 2026 obs-webkit-browser contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU General Public License as published by the Free Software
|
||||
Foundation, either version 2 of the License, or (at your option) any later
|
||||
version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
53
README.md
Normal file
53
README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# OBS WebKit Browser Source (без CEF)
|
||||
|
||||
Нативный input source для OBS Studio на Alpine Linux. Страницы рендерит системный
|
||||
WebKitGTK 4.1 в отдельном процессе на приватном Xvfb-дисплее; Chromium/CEF в проекте
|
||||
нет. Кадры передаются в OBS напрямую как BGRA, без FFmpeg и захвата рабочего стола.
|
||||
|
||||
Поддерживается:
|
||||
|
||||
- HTTP/HTTPS, `data:` URL и локальные HTML-файлы;
|
||||
- прозрачный фон с alpha-каналом;
|
||||
- пользовательский CSS;
|
||||
- размер 2–8192 px и 1–60 FPS;
|
||||
- аппаратный WebKit compositor через DRM/DMA-BUF (Mesa `radeonsi` на AMD),
|
||||
при этом Xvfb используется только как изолированный невидимый дисплей;
|
||||
- прямой XShm-захват итогового кадра Xvfb для непрозрачных страниц и
|
||||
XComposite/XShm-захват с alpha для прозрачных;
|
||||
- shared-memory транспорт кадров без многомегабайтной записи в pipe;
|
||||
- мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS;
|
||||
- перезагрузка страницы и остановка renderer-процесса у скрытого source.
|
||||
|
||||
Звук страницы намеренно выключен: добавьте в OBS отдельный Application Audio Capture.
|
||||
Renderer всегда использует отдельный приватный Xvfb-дисплей и не создаёт окон на
|
||||
рабочем столе. WebKit принудительно включает аппаратный compositor и выбирает доступный
|
||||
DRM render node; для него нужен доступ к `/dev/dri/renderD*`.
|
||||
Для тяжёлого полноэкранного `filter: blur()` всё ещё разумно выбирать реальный размер
|
||||
виджета вместо рендера 4K/1080p с последующим уменьшением в сцене. Пользовательские
|
||||
`will-change` и compositor hints сохраняются, но renderer не внедряет их в страницу сам:
|
||||
WebKit сам распределяет элементы по GPU-слоям.
|
||||
|
||||
Это рабочий backend, а не замена всех расширенных возможностей штатного obs-browser
|
||||
(DevTools, cookies API, browser docks и DRM здесь пока отсутствуют).
|
||||
|
||||
## Сборка на Alpine
|
||||
|
||||
```sh
|
||||
chmod +x scripts/install-deps-alpine.sh
|
||||
./scripts/install-deps-alpine.sh
|
||||
/usr/bin/cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
/usr/bin/cmake --build build
|
||||
ctest --test-dir build --output-on-failure
|
||||
doas /usr/bin/cmake --install build --prefix /usr
|
||||
```
|
||||
|
||||
После установки перезапустите OBS и добавьте источник **Браузер WebKit (без CEF)**.
|
||||
Если OBS установлен в нестандартный prefix, задайте при конфигурации
|
||||
`OBS_PLUGIN_DIR` и `OBS_PLUGIN_DATA_DIR`.
|
||||
|
||||
## Почему WebKitGTK, а не WPE
|
||||
|
||||
WPE архитектурно подходит лучше всего, но пакет `wpewebkit` отсутствует в актуальном
|
||||
Alpine edge/3.24 (остались только `libwpe` и backend). WebKitGTK 4.1 есть в текущем
|
||||
репозитории Alpine, использует тот же WebKit и не зависит от CEF. Renderer изолирован
|
||||
от OBS, поэтому падение Web-процесса не должно уронить студию.
|
||||
9
data/locale/en-US.ini
Normal file
9
data/locale/en-US.ini
Normal file
@@ -0,0 +1,9 @@
|
||||
WebKitBrowserSource="WebKit Browser (no CEF)"
|
||||
URL="URL or local HTML file"
|
||||
Width="Width"
|
||||
Height="Height"
|
||||
FPS="Frames per second"
|
||||
Transparent="Transparent background"
|
||||
CustomCSS="Custom CSS"
|
||||
ShutdownWhenHidden="Shut down renderer when source is hidden"
|
||||
Reload="Reload page"
|
||||
9
data/locale/ru-RU.ini
Normal file
9
data/locale/ru-RU.ini
Normal file
@@ -0,0 +1,9 @@
|
||||
WebKitBrowserSource="Браузер WebKit (без CEF)"
|
||||
URL="URL или локальный HTML-файл"
|
||||
Width="Ширина"
|
||||
Height="Высота"
|
||||
FPS="Кадров в секунду"
|
||||
Transparent="Прозрачный фон"
|
||||
CustomCSS="Пользовательский CSS"
|
||||
ShutdownWhenHidden="Останавливать рендерер, когда источник скрыт"
|
||||
Reload="Перезагрузить страницу"
|
||||
21
scripts/install-deps-alpine.sh
Executable file
21
scripts/install-deps-alpine.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
ROOT=
|
||||
elif command -v doas >/dev/null 2>&1; then
|
||||
ROOT=doas
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
ROOT=sudo
|
||||
else
|
||||
echo "Run as root or install doas/sudo." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
$ROOT apk add \
|
||||
build-base cmake samurai pkgconf \
|
||||
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 \
|
||||
xvfb mesa-dri-gallium
|
||||
607
src/plugin.c
Normal file
607
src/plugin.c
Normal file
@@ -0,0 +1,607 @@
|
||||
#include <obs/obs-module.h>
|
||||
#include <obs/obs-interaction.h>
|
||||
#include <obs/util/platform.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <spawn.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "protocol.h"
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
OBS_MODULE_USE_DEFAULT_LOCALE("obs-webkit-browser", "en-US")
|
||||
MODULE_EXPORT const char *obs_module_description(void)
|
||||
{
|
||||
return "WebKitGTK browser source for OBS (no CEF)";
|
||||
}
|
||||
|
||||
extern char **environ;
|
||||
|
||||
struct browser_source;
|
||||
|
||||
struct renderer_process {
|
||||
pid_t pid;
|
||||
int control_fd;
|
||||
int frame_fd;
|
||||
int log_fd;
|
||||
int shared_fd;
|
||||
struct owb_shared_frames *shared;
|
||||
size_t shared_size;
|
||||
pthread_t frame_thread;
|
||||
pthread_t log_thread;
|
||||
bool frame_thread_started;
|
||||
bool log_thread_started;
|
||||
atomic_bool stopping;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
struct browser_source *owner;
|
||||
};
|
||||
|
||||
struct browser_source {
|
||||
obs_source_t *source;
|
||||
pthread_mutex_t mutex;
|
||||
struct renderer_process *renderer;
|
||||
char *url;
|
||||
char *css;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t fps;
|
||||
bool transparent;
|
||||
bool shutdown_when_hidden;
|
||||
bool visible;
|
||||
};
|
||||
|
||||
static bool write_all_socket(int fd, const void *data, size_t size)
|
||||
{
|
||||
const uint8_t *cursor = data;
|
||||
while (size > 0) {
|
||||
ssize_t written = send(fd, cursor, size, MSG_NOSIGNAL);
|
||||
if (written < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (written <= 0)
|
||||
return false;
|
||||
cursor += (size_t)written;
|
||||
size -= (size_t)written;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void set_close_on_exec(int fd)
|
||||
{
|
||||
int flags = fcntl(fd, F_GETFD);
|
||||
if (flags >= 0)
|
||||
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
|
||||
}
|
||||
|
||||
static uint32_t setting_u32(obs_data_t *settings, const char *name, uint32_t minimum,
|
||||
uint32_t maximum)
|
||||
{
|
||||
long long value = obs_data_get_int(settings, name);
|
||||
if (value < (long long)minimum)
|
||||
return minimum;
|
||||
if (value > (long long)maximum)
|
||||
return maximum;
|
||||
return (uint32_t)value;
|
||||
}
|
||||
|
||||
static void *frame_reader(void *opaque)
|
||||
{
|
||||
struct renderer_process *renderer = opaque;
|
||||
size_t stride = (size_t)renderer->width * 4;
|
||||
size_t frame_size = owb_frame_size(renderer->width, renderer->height);
|
||||
uint8_t *pixels = malloc(frame_size);
|
||||
if (!pixels) {
|
||||
blog(LOG_ERROR, "[obs-webkit-browser] Unable to allocate a %zu-byte frame", frame_size);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
while (!atomic_load(&renderer->stopping)) {
|
||||
struct pollfd notification = {.fd = renderer->frame_fd, .events = POLLIN};
|
||||
int ready = poll(¬ification, 1, 200);
|
||||
if (ready < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (ready <= 0)
|
||||
continue;
|
||||
uint8_t slot;
|
||||
ssize_t count = read(renderer->frame_fd, &slot, sizeof(slot));
|
||||
if (count < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (count <= 0)
|
||||
break;
|
||||
if (slot >= OWB_SHARED_FRAME_SLOTS)
|
||||
continue;
|
||||
bool stable = false;
|
||||
for (unsigned attempt = 0; attempt < 3 && !stable; attempt++) {
|
||||
uint64_t before = __atomic_load_n(&renderer->shared->sequence[slot], __ATOMIC_ACQUIRE);
|
||||
if (before & 1u)
|
||||
continue;
|
||||
memcpy(pixels, owb_shared_slot(renderer->shared, slot), frame_size);
|
||||
uint64_t after = __atomic_load_n(&renderer->shared->sequence[slot], __ATOMIC_ACQUIRE);
|
||||
stable = before == after && !(after & 1u);
|
||||
}
|
||||
if (!stable)
|
||||
continue;
|
||||
struct obs_source_frame frame = {0};
|
||||
frame.data[0] = pixels;
|
||||
frame.linesize[0] = (uint32_t)stride;
|
||||
frame.width = renderer->width;
|
||||
frame.height = renderer->height;
|
||||
frame.timestamp = os_gettime_ns();
|
||||
frame.format = VIDEO_FORMAT_BGRA;
|
||||
frame.full_range = true;
|
||||
obs_source_output_video(renderer->owner->source, &frame);
|
||||
}
|
||||
|
||||
free(pixels);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *log_reader(void *opaque)
|
||||
{
|
||||
struct renderer_process *renderer = opaque;
|
||||
char line[2048];
|
||||
size_t used = 0;
|
||||
while (!atomic_load(&renderer->stopping)) {
|
||||
struct pollfd item = {.fd = renderer->log_fd, .events = POLLIN};
|
||||
int ready = poll(&item, 1, 200);
|
||||
if (ready < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (ready <= 0)
|
||||
continue;
|
||||
char chunk[512];
|
||||
ssize_t count = read(renderer->log_fd, chunk, sizeof(chunk));
|
||||
if (count <= 0)
|
||||
break;
|
||||
for (ssize_t index = 0; index < count; index++) {
|
||||
char byte = chunk[index];
|
||||
if (byte == '\n') {
|
||||
line[used] = '\0';
|
||||
if (used)
|
||||
blog(LOG_INFO, "[obs-webkit-browser renderer] %s", line);
|
||||
used = 0;
|
||||
} else if (byte != '\r' && used + 1 < sizeof(line)) {
|
||||
line[used++] = byte;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (used) {
|
||||
line[used] = '\0';
|
||||
blog(LOG_INFO, "[obs-webkit-browser renderer] %s", line);
|
||||
}
|
||||
close(renderer->log_fd);
|
||||
renderer->log_fd = -1;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void terminate_renderer_group(pid_t pid)
|
||||
{
|
||||
if (pid <= 0)
|
||||
return;
|
||||
kill(-pid, SIGTERM);
|
||||
for (unsigned attempt = 0; attempt < 200; attempt++) {
|
||||
pid_t result = waitpid(pid, NULL, WNOHANG);
|
||||
if (result == pid || (result < 0 && errno == ECHILD))
|
||||
return;
|
||||
struct timespec pause = {.tv_nsec = 10000000L};
|
||||
nanosleep(&pause, NULL);
|
||||
}
|
||||
kill(-pid, SIGKILL);
|
||||
while (waitpid(pid, NULL, 0) < 0 && errno == EINTR) {
|
||||
}
|
||||
}
|
||||
|
||||
static void stop_renderer_locked(struct browser_source *browser)
|
||||
{
|
||||
struct renderer_process *renderer = browser->renderer;
|
||||
if (!renderer)
|
||||
return;
|
||||
browser->renderer = NULL;
|
||||
atomic_store(&renderer->stopping, true);
|
||||
|
||||
if (renderer->control_fd >= 0) {
|
||||
shutdown(renderer->control_fd, SHUT_RDWR);
|
||||
close(renderer->control_fd);
|
||||
renderer->control_fd = -1;
|
||||
}
|
||||
terminate_renderer_group(renderer->pid);
|
||||
if (renderer->frame_fd >= 0) {
|
||||
close(renderer->frame_fd);
|
||||
renderer->frame_fd = -1;
|
||||
}
|
||||
if (renderer->frame_thread_started)
|
||||
pthread_join(renderer->frame_thread, NULL);
|
||||
if (renderer->log_thread_started)
|
||||
pthread_join(renderer->log_thread, NULL);
|
||||
else if (renderer->log_fd >= 0)
|
||||
close(renderer->log_fd);
|
||||
if (renderer->shared && renderer->shared != MAP_FAILED)
|
||||
munmap(renderer->shared, renderer->shared_size);
|
||||
if (renderer->shared_fd >= 0)
|
||||
close(renderer->shared_fd);
|
||||
free(renderer);
|
||||
}
|
||||
|
||||
static bool send_initial_config(struct browser_source *browser, struct renderer_process *renderer)
|
||||
{
|
||||
size_t url_length = strlen(browser->url);
|
||||
size_t css_length = strlen(browser->css);
|
||||
if (url_length > OWB_MAX_STRING || css_length > OWB_MAX_STRING)
|
||||
return false;
|
||||
struct owb_config_header header = {
|
||||
.magic = OWB_CONFIG_MAGIC,
|
||||
.version = OWB_PROTOCOL_VERSION,
|
||||
.width = browser->width,
|
||||
.height = browser->height,
|
||||
.fps = browser->fps,
|
||||
.transparent = browser->transparent,
|
||||
.hardware_acceleration = true,
|
||||
.url_length = (uint32_t)url_length,
|
||||
.css_length = (uint32_t)css_length,
|
||||
};
|
||||
return write_all_socket(renderer->control_fd, &header, sizeof(header)) &&
|
||||
write_all_socket(renderer->control_fd, browser->url, url_length) &&
|
||||
write_all_socket(renderer->control_fd, browser->css, css_length);
|
||||
}
|
||||
|
||||
static bool start_renderer_locked(struct browser_source *browser)
|
||||
{
|
||||
if (browser->renderer || (browser->shutdown_when_hidden && !browser->visible))
|
||||
return true;
|
||||
|
||||
char *helper = obs_module_file("obs-webkit-renderer");
|
||||
if (!helper) {
|
||||
blog(LOG_ERROR, "[obs-webkit-browser] Renderer helper is missing from the plugin data directory");
|
||||
return false;
|
||||
}
|
||||
if (access(helper, X_OK) != 0) {
|
||||
blog(LOG_ERROR, "[obs-webkit-browser] Renderer is not executable: %s", helper);
|
||||
bfree(helper);
|
||||
return false;
|
||||
}
|
||||
|
||||
int control[2] = {-1, -1};
|
||||
int frames[2] = {-1, -1};
|
||||
int logs[2] = {-1, -1};
|
||||
int shared_fd = -1;
|
||||
struct owb_shared_frames *shared = MAP_FAILED;
|
||||
size_t shared_size = owb_shared_size(browser->width, browser->height);
|
||||
shared_fd = memfd_create("obs-webkit-browser-frames", MFD_CLOEXEC);
|
||||
if (shared_fd < 0 || ftruncate(shared_fd, (off_t)shared_size) != 0) {
|
||||
blog(LOG_ERROR, "[obs-webkit-browser] Shared frame allocation failed: %s", strerror(errno));
|
||||
goto fail;
|
||||
}
|
||||
shared = mmap(NULL, shared_size, PROT_READ | PROT_WRITE, MAP_SHARED, shared_fd, 0);
|
||||
if (shared == MAP_FAILED) {
|
||||
blog(LOG_ERROR, "[obs-webkit-browser] Shared frame mapping failed: %s", strerror(errno));
|
||||
goto fail;
|
||||
}
|
||||
memset(shared, 0, sizeof(*shared));
|
||||
shared->magic = OWB_SHARED_MAGIC;
|
||||
shared->version = OWB_PROTOCOL_VERSION;
|
||||
shared->width = browser->width;
|
||||
shared->height = browser->height;
|
||||
shared->stride = browser->width * 4;
|
||||
shared->slots = OWB_SHARED_FRAME_SLOTS;
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) != 0 || pipe(frames) != 0 || pipe(logs) != 0) {
|
||||
blog(LOG_ERROR, "[obs-webkit-browser] IPC setup failed: %s", strerror(errno));
|
||||
goto fail;
|
||||
}
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
set_close_on_exec(control[i]);
|
||||
set_close_on_exec(frames[i]);
|
||||
set_close_on_exec(logs[i]);
|
||||
}
|
||||
|
||||
posix_spawn_file_actions_t actions;
|
||||
posix_spawnattr_t attributes;
|
||||
posix_spawn_file_actions_init(&actions);
|
||||
posix_spawnattr_init(&attributes);
|
||||
sigset_t defaults;
|
||||
sigemptyset(&defaults);
|
||||
sigaddset(&defaults, SIGTERM);
|
||||
sigaddset(&defaults, SIGINT);
|
||||
sigaddset(&defaults, SIGPIPE);
|
||||
posix_spawnattr_setsigdefault(&attributes, &defaults);
|
||||
posix_spawnattr_setpgroup(&attributes, 0);
|
||||
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETPGROUP);
|
||||
posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&actions, frames[1], STDOUT_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&actions, logs[1], STDERR_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&actions, shared_fd, OWB_SHARED_FRAME_FD);
|
||||
posix_spawn_file_actions_addclose(&actions, control[0]);
|
||||
posix_spawn_file_actions_addclose(&actions, frames[0]);
|
||||
posix_spawn_file_actions_addclose(&actions, logs[0]);
|
||||
char *argv[] = {helper, NULL};
|
||||
pid_t pid = 0;
|
||||
int spawn_error = posix_spawn(&pid, helper, &actions, &attributes, argv, environ);
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
posix_spawnattr_destroy(&attributes);
|
||||
if (spawn_error != 0) {
|
||||
blog(LOG_ERROR, "[obs-webkit-browser] Cannot start renderer: %s", strerror(spawn_error));
|
||||
goto fail;
|
||||
}
|
||||
|
||||
close(control[1]);
|
||||
close(frames[1]);
|
||||
close(logs[1]);
|
||||
control[1] = frames[1] = logs[1] = -1;
|
||||
struct renderer_process *renderer = calloc(1, sizeof(*renderer));
|
||||
if (!renderer) {
|
||||
terminate_renderer_group(pid);
|
||||
goto fail;
|
||||
}
|
||||
renderer->pid = pid;
|
||||
renderer->control_fd = control[0];
|
||||
renderer->frame_fd = frames[0];
|
||||
renderer->log_fd = logs[0];
|
||||
renderer->shared_fd = shared_fd;
|
||||
renderer->shared = shared;
|
||||
renderer->shared_size = shared_size;
|
||||
renderer->width = browser->width;
|
||||
renderer->height = browser->height;
|
||||
renderer->owner = browser;
|
||||
atomic_init(&renderer->stopping, false);
|
||||
control[0] = frames[0] = logs[0] = -1;
|
||||
shared_fd = -1;
|
||||
shared = MAP_FAILED;
|
||||
|
||||
if (!send_initial_config(browser, renderer)) {
|
||||
blog(LOG_ERROR, "[obs-webkit-browser] Cannot send configuration to renderer");
|
||||
terminate_renderer_group(renderer->pid);
|
||||
close(renderer->control_fd);
|
||||
close(renderer->frame_fd);
|
||||
close(renderer->log_fd);
|
||||
munmap(renderer->shared, renderer->shared_size);
|
||||
close(renderer->shared_fd);
|
||||
free(renderer);
|
||||
goto fail;
|
||||
}
|
||||
if (pthread_create(&renderer->frame_thread, NULL, frame_reader, renderer) == 0)
|
||||
renderer->frame_thread_started = true;
|
||||
if (pthread_create(&renderer->log_thread, NULL, log_reader, renderer) == 0)
|
||||
renderer->log_thread_started = true;
|
||||
if (!renderer->frame_thread_started) {
|
||||
blog(LOG_ERROR, "[obs-webkit-browser] Cannot start frame reader thread");
|
||||
browser->renderer = renderer;
|
||||
stop_renderer_locked(browser);
|
||||
goto fail;
|
||||
}
|
||||
browser->renderer = renderer;
|
||||
blog(LOG_INFO, "[obs-webkit-browser] Started %ux%u@%u renderer", browser->width,
|
||||
browser->height, browser->fps);
|
||||
bfree(helper);
|
||||
return true;
|
||||
|
||||
fail:
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
if (control[i] >= 0)
|
||||
close(control[i]);
|
||||
if (frames[i] >= 0)
|
||||
close(frames[i]);
|
||||
if (logs[i] >= 0)
|
||||
close(logs[i]);
|
||||
}
|
||||
bfree(helper);
|
||||
if (shared != MAP_FAILED)
|
||||
munmap(shared, shared_size);
|
||||
if (shared_fd >= 0)
|
||||
close(shared_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void restart_renderer(struct browser_source *browser)
|
||||
{
|
||||
pthread_mutex_lock(&browser->mutex);
|
||||
stop_renderer_locked(browser);
|
||||
start_renderer_locked(browser);
|
||||
pthread_mutex_unlock(&browser->mutex);
|
||||
}
|
||||
|
||||
static void browser_update(void *data, obs_data_t *settings)
|
||||
{
|
||||
struct browser_source *browser = data;
|
||||
const char *url = obs_data_get_string(settings, "url");
|
||||
const char *css = obs_data_get_string(settings, "css");
|
||||
char *new_url = bstrdup(url && *url ? url : "about:blank");
|
||||
char *new_css = bstrdup(css ? css : "");
|
||||
pthread_mutex_lock(&browser->mutex);
|
||||
bfree(browser->url);
|
||||
bfree(browser->css);
|
||||
browser->url = new_url;
|
||||
browser->css = new_css;
|
||||
browser->width = setting_u32(settings, "width", 2, 8192);
|
||||
browser->height = setting_u32(settings, "height", 2, 8192);
|
||||
browser->fps = setting_u32(settings, "fps", 1, 60);
|
||||
browser->transparent = obs_data_get_bool(settings, "transparent");
|
||||
browser->shutdown_when_hidden = obs_data_get_bool(settings, "shutdown_when_hidden");
|
||||
stop_renderer_locked(browser);
|
||||
start_renderer_locked(browser);
|
||||
pthread_mutex_unlock(&browser->mutex);
|
||||
}
|
||||
|
||||
static void *browser_create(obs_data_t *settings, obs_source_t *source)
|
||||
{
|
||||
struct browser_source *browser = bzalloc(sizeof(*browser));
|
||||
browser->source = source;
|
||||
browser->visible = true;
|
||||
pthread_mutex_init(&browser->mutex, NULL);
|
||||
browser_update(browser, settings);
|
||||
return browser;
|
||||
}
|
||||
|
||||
static void browser_destroy(void *data)
|
||||
{
|
||||
struct browser_source *browser = data;
|
||||
pthread_mutex_lock(&browser->mutex);
|
||||
stop_renderer_locked(browser);
|
||||
pthread_mutex_unlock(&browser->mutex);
|
||||
pthread_mutex_destroy(&browser->mutex);
|
||||
bfree(browser->url);
|
||||
bfree(browser->css);
|
||||
bfree(browser);
|
||||
}
|
||||
|
||||
static uint32_t browser_width(void *data)
|
||||
{
|
||||
return ((struct browser_source *)data)->width;
|
||||
}
|
||||
|
||||
static uint32_t browser_height(void *data)
|
||||
{
|
||||
return ((struct browser_source *)data)->height;
|
||||
}
|
||||
|
||||
static void browser_defaults(obs_data_t *settings)
|
||||
{
|
||||
obs_data_set_default_string(settings, "url", "https://example.com");
|
||||
obs_data_set_default_int(settings, "width", 1280);
|
||||
obs_data_set_default_int(settings, "height", 720);
|
||||
obs_data_set_default_int(settings, "fps", 30);
|
||||
obs_data_set_default_bool(settings, "transparent", true);
|
||||
obs_data_set_default_bool(settings, "shutdown_when_hidden", false);
|
||||
}
|
||||
|
||||
static bool reload_clicked(obs_properties_t *props, obs_property_t *property, void *data)
|
||||
{
|
||||
UNUSED_PARAMETER(props);
|
||||
UNUSED_PARAMETER(property);
|
||||
restart_renderer(data);
|
||||
return false;
|
||||
}
|
||||
|
||||
static obs_properties_t *browser_properties(void *data)
|
||||
{
|
||||
obs_properties_t *props = obs_properties_create();
|
||||
obs_properties_add_text(props, "url", obs_module_text("URL"), OBS_TEXT_DEFAULT);
|
||||
obs_properties_add_int(props, "width", obs_module_text("Width"), 2, 8192, 1);
|
||||
obs_properties_add_int(props, "height", obs_module_text("Height"), 2, 8192, 1);
|
||||
obs_properties_add_int(props, "fps", obs_module_text("FPS"), 1, 60, 1);
|
||||
obs_properties_add_bool(props, "transparent", obs_module_text("Transparent"));
|
||||
obs_properties_add_text(props, "css", obs_module_text("CustomCSS"), OBS_TEXT_MULTILINE);
|
||||
obs_properties_add_bool(props, "shutdown_when_hidden", obs_module_text("ShutdownWhenHidden"));
|
||||
if (data)
|
||||
obs_properties_add_button2(props, "reload", obs_module_text("Reload"), reload_clicked, data);
|
||||
return props;
|
||||
}
|
||||
|
||||
static void send_command(struct browser_source *browser, struct owb_command *command)
|
||||
{
|
||||
command->magic = OWB_COMMAND_MAGIC;
|
||||
pthread_mutex_lock(&browser->mutex);
|
||||
if (browser->renderer && browser->renderer->control_fd >= 0)
|
||||
write_all_socket(browser->renderer->control_fd, command, sizeof(*command));
|
||||
pthread_mutex_unlock(&browser->mutex);
|
||||
}
|
||||
|
||||
static void browser_mouse_move(void *data, const struct obs_mouse_event *event, bool leave)
|
||||
{
|
||||
struct owb_command command = {.type = OWB_MOUSE_MOVE, .x = event->x, .y = event->y,
|
||||
.value1 = leave, .modifiers = event->modifiers};
|
||||
send_command(data, &command);
|
||||
}
|
||||
|
||||
static void browser_mouse_click(void *data, const struct obs_mouse_event *event, int32_t type,
|
||||
bool mouse_up, uint32_t click_count)
|
||||
{
|
||||
struct owb_command command = {.type = OWB_MOUSE_CLICK, .x = event->x, .y = event->y,
|
||||
.value1 = type, .value2 = mouse_up,
|
||||
.modifiers = event->modifiers};
|
||||
command.text_length = click_count;
|
||||
send_command(data, &command);
|
||||
}
|
||||
|
||||
static void browser_mouse_wheel(void *data, const struct obs_mouse_event *event, int x_delta, int y_delta)
|
||||
{
|
||||
struct owb_command command = {.type = OWB_MOUSE_WHEEL, .x = event->x, .y = event->y,
|
||||
.value1 = x_delta, .value2 = y_delta,
|
||||
.modifiers = event->modifiers};
|
||||
send_command(data, &command);
|
||||
}
|
||||
|
||||
static void browser_focus(void *data, bool focus)
|
||||
{
|
||||
struct owb_command command = {.type = OWB_FOCUS, .value1 = focus};
|
||||
send_command(data, &command);
|
||||
}
|
||||
|
||||
static void browser_key(void *data, const struct obs_key_event *event, bool key_up)
|
||||
{
|
||||
struct owb_command command = {.type = OWB_KEY, .value1 = key_up,
|
||||
.value2 = (int32_t)event->native_vkey,
|
||||
.modifiers = event->modifiers};
|
||||
if (event->text) {
|
||||
size_t length = strlen(event->text);
|
||||
if (length > sizeof(command.text))
|
||||
length = sizeof(command.text);
|
||||
memcpy(command.text, event->text, length);
|
||||
command.text_length = (uint32_t)length;
|
||||
}
|
||||
send_command(data, &command);
|
||||
}
|
||||
|
||||
static void browser_show(void *data)
|
||||
{
|
||||
struct browser_source *browser = data;
|
||||
pthread_mutex_lock(&browser->mutex);
|
||||
browser->visible = true;
|
||||
start_renderer_locked(browser);
|
||||
pthread_mutex_unlock(&browser->mutex);
|
||||
}
|
||||
|
||||
static void browser_hide(void *data)
|
||||
{
|
||||
struct browser_source *browser = data;
|
||||
pthread_mutex_lock(&browser->mutex);
|
||||
browser->visible = false;
|
||||
if (browser->shutdown_when_hidden)
|
||||
stop_renderer_locked(browser);
|
||||
pthread_mutex_unlock(&browser->mutex);
|
||||
}
|
||||
|
||||
static const char *browser_name(void *unused)
|
||||
{
|
||||
UNUSED_PARAMETER(unused);
|
||||
return obs_module_text("WebKitBrowserSource");
|
||||
}
|
||||
|
||||
static struct obs_source_info browser_info = {
|
||||
.id = "webkit_browser_source",
|
||||
.type = OBS_SOURCE_TYPE_INPUT,
|
||||
.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_INTERACTION,
|
||||
.get_name = browser_name,
|
||||
.create = browser_create,
|
||||
.destroy = browser_destroy,
|
||||
.update = browser_update,
|
||||
.get_defaults = browser_defaults,
|
||||
.get_properties = browser_properties,
|
||||
.get_width = browser_width,
|
||||
.get_height = browser_height,
|
||||
.show = browser_show,
|
||||
.hide = browser_hide,
|
||||
.mouse_click = browser_mouse_click,
|
||||
.mouse_move = browser_mouse_move,
|
||||
.mouse_wheel = browser_mouse_wheel,
|
||||
.focus = browser_focus,
|
||||
.key_click = browser_key,
|
||||
};
|
||||
|
||||
bool obs_module_load(void)
|
||||
{
|
||||
obs_register_source(&browser_info);
|
||||
blog(LOG_INFO, "[obs-webkit-browser] Loaded WebKitGTK browser source");
|
||||
return true;
|
||||
}
|
||||
73
src/protocol.h
Normal file
73
src/protocol.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define OWB_CONFIG_MAGIC 0x4f574243u /* OWBC */
|
||||
#define OWB_COMMAND_MAGIC 0x4f574245u /* OWBE */
|
||||
#define OWB_PROTOCOL_VERSION 2u
|
||||
#define OWB_MAX_STRING (16u * 1024u * 1024u)
|
||||
#define OWB_SHARED_MAGIC 0x4f574246u /* OWBF */
|
||||
#define OWB_SHARED_FRAME_FD 198
|
||||
#define OWB_SHARED_FRAME_SLOTS 2u
|
||||
|
||||
struct owb_config_header {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t fps;
|
||||
uint32_t transparent;
|
||||
uint32_t hardware_acceleration;
|
||||
uint32_t url_length;
|
||||
uint32_t css_length;
|
||||
};
|
||||
|
||||
struct owb_shared_frames {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t stride;
|
||||
uint32_t slots;
|
||||
uint64_t sequence[OWB_SHARED_FRAME_SLOTS];
|
||||
uint8_t pixels[];
|
||||
};
|
||||
|
||||
static inline size_t owb_frame_size(uint32_t width, uint32_t height)
|
||||
{
|
||||
return (size_t)width * (size_t)height * 4u;
|
||||
}
|
||||
|
||||
static inline size_t owb_shared_size(uint32_t width, uint32_t height)
|
||||
{
|
||||
return sizeof(struct owb_shared_frames) +
|
||||
owb_frame_size(width, height) * OWB_SHARED_FRAME_SLOTS;
|
||||
}
|
||||
|
||||
static inline uint8_t *owb_shared_slot(struct owb_shared_frames *shared, uint32_t slot)
|
||||
{
|
||||
return shared->pixels + owb_frame_size(shared->width, shared->height) * slot;
|
||||
}
|
||||
|
||||
enum owb_command_type {
|
||||
OWB_MOUSE_MOVE = 1,
|
||||
OWB_MOUSE_CLICK = 2,
|
||||
OWB_MOUSE_WHEEL = 3,
|
||||
OWB_FOCUS = 4,
|
||||
OWB_KEY = 5,
|
||||
OWB_RELOAD = 6,
|
||||
};
|
||||
|
||||
/* Fixed-size control messages keep the renderer's nonblocking parser simple. */
|
||||
struct owb_command {
|
||||
uint32_t magic;
|
||||
uint32_t type;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
int32_t value1;
|
||||
int32_t value2;
|
||||
uint32_t modifiers;
|
||||
uint32_t text_length;
|
||||
char text[64];
|
||||
};
|
||||
645
src/renderer.c
Normal file
645
src/renderer.c
Normal file
@@ -0,0 +1,645 @@
|
||||
#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 <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <poll.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 <sys/types.h>
|
||||
#include <sys/wait.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;
|
||||
GPid xvfb_pid;
|
||||
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;
|
||||
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 used this flag for an experimental host-display mode. It is
|
||||
* intentionally ignored: an OBS source must never create an input window on
|
||||
* the user's desktop. */
|
||||
(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 bool prepare_display(struct renderer *renderer)
|
||||
{
|
||||
char screen[64];
|
||||
g_snprintf(screen, sizeof(screen), "%ux%ux24", renderer->config.width, renderer->config.height);
|
||||
char *argv[] = {"Xvfb", "-displayfd", "1", "-screen", "0", screen, "+extension", "Composite",
|
||||
"-nolisten", "tcp", "-noreset", "-dpi", "96", NULL};
|
||||
int stdout_fd = -1;
|
||||
GError *error = NULL;
|
||||
if (!g_spawn_async_with_pipes(NULL, argv, NULL,
|
||||
G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL,
|
||||
&renderer->xvfb_pid, NULL, &stdout_fd, NULL, &error)) {
|
||||
fprintf(stderr, "Cannot start Xvfb: %s\n", error->message);
|
||||
g_error_free(error);
|
||||
return false;
|
||||
}
|
||||
char display_number[32] = {0};
|
||||
size_t display_used = 0;
|
||||
while (display_used + 1 < sizeof(display_number) &&
|
||||
!memchr(display_number, '\n', display_used)) {
|
||||
struct pollfd display_poll = {.fd = stdout_fd, .events = POLLIN};
|
||||
int ready;
|
||||
do {
|
||||
ready = poll(&display_poll, 1, 10000);
|
||||
} while (ready < 0 && errno == EINTR);
|
||||
if (ready <= 0)
|
||||
break;
|
||||
ssize_t count = read(stdout_fd, display_number + display_used,
|
||||
sizeof(display_number) - display_used - 1);
|
||||
if (count <= 0)
|
||||
break;
|
||||
display_used += (size_t)count;
|
||||
}
|
||||
close(stdout_fd);
|
||||
if (!display_used || !memchr(display_number, '\n', display_used)) {
|
||||
fprintf(stderr, "Xvfb did not publish a display number within 10 seconds\n");
|
||||
kill(renderer->xvfb_pid, SIGTERM);
|
||||
waitpid(renderer->xvfb_pid, NULL, 0);
|
||||
g_spawn_close_pid(renderer->xvfb_pid);
|
||||
renderer->xvfb_pid = 0;
|
||||
return false;
|
||||
}
|
||||
display_number[strcspn(display_number, "\r\n")] = '\0';
|
||||
char *display = g_strdup_printf(":%s", display_number);
|
||||
g_setenv("DISPLAY", display, TRUE);
|
||||
g_setenv("GDK_BACKEND", "x11", TRUE);
|
||||
g_free(display);
|
||||
/* WebKitGTK's accelerated backing store renders into a DRM DMA-BUF and maps
|
||||
* the completed buffer into this private X display. Xvfb remains useful for
|
||||
* keeping the Gtk window off the desktop; it must not force WebKit itself to
|
||||
* use llvmpipe. Override software-rendering flags inherited from OBS too. */
|
||||
g_unsetenv("LIBGL_ALWAYS_SOFTWARE");
|
||||
g_unsetenv("GALLIUM_DRIVER");
|
||||
g_unsetenv("MESA_LOADER_DRIVER_OVERRIDE");
|
||||
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", FALSE);
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/* 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
|
||||
* drawable contains the final assembled image, and the renderer window
|
||||
* covers that private screen exactly. */
|
||||
if (!renderer->config.transparent) {
|
||||
renderer->capture_window = DefaultRootWindow(renderer->capture_display);
|
||||
renderer->capture_pixmap = renderer->capture_window;
|
||||
} else {
|
||||
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);
|
||||
int event_base = 0, error_base = 0;
|
||||
if (!XCompositeQueryExtension(renderer->capture_display, &event_base, &error_base))
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
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_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 && renderer->capture_redirected)
|
||||
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);
|
||||
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;
|
||||
}
|
||||
__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 void publish_x11_frame(struct renderer *renderer)
|
||||
{
|
||||
if (!XShmGetImage(renderer->capture_display, renderer->capture_pixmap,
|
||||
renderer->capture_image, 0, 0, AllPlanes))
|
||||
return;
|
||||
publish_frame(renderer, (const uint8_t *)renderer->capture_image->data,
|
||||
renderer->capture_image->bytes_per_line,
|
||||
renderer->config.transparent && renderer->capture_image->depth == 32);
|
||||
}
|
||||
|
||||
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);
|
||||
while (!atomic_load_explicit(&renderer->stopping, memory_order_acquire)) {
|
||||
publish_x11_frame(renderer);
|
||||
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;
|
||||
}
|
||||
if (!prepare_display(&renderer))
|
||||
return 3;
|
||||
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 private Xvfb 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.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) {
|
||||
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);
|
||||
}
|
||||
gtk_widget_show_all(renderer.window);
|
||||
while (gtk_events_pending())
|
||||
gtk_main_iteration();
|
||||
if (!initialize_x11_capture(&renderer)) {
|
||||
fprintf(stderr, "Cannot initialize XComposite/XShm capture\n");
|
||||
return 6;
|
||||
}
|
||||
|
||||
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 X11 capture thread\n");
|
||||
return 7;
|
||||
}
|
||||
renderer.capture_thread_started = true;
|
||||
fprintf(stderr, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, GPU compositing, DMA-BUF, X11 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);
|
||||
if (renderer.xvfb_pid > 0) {
|
||||
kill(renderer.xvfb_pid, SIGTERM);
|
||||
waitpid(renderer.xvfb_pid, NULL, 0);
|
||||
g_spawn_close_pid(renderer.xvfb_pid);
|
||||
}
|
||||
munmap(renderer.shared, renderer.shared_size);
|
||||
free(renderer.config.url);
|
||||
free(renderer.config.css);
|
||||
return 0;
|
||||
}
|
||||
70
tests/module-load-test.c
Normal file
70
tests/module-load-test.c
Normal file
@@ -0,0 +1,70 @@
|
||||
#include <obs/obs.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc != 4) {
|
||||
fprintf(stderr, "usage: %s plugin.so data-directory renderer\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
char temporary[] = "/tmp/obs-webkit-module-test-XXXXXX";
|
||||
if (!mkdtemp(temporary)) {
|
||||
perror("mkdtemp");
|
||||
return 2;
|
||||
}
|
||||
char renderer_link[512];
|
||||
char locale_link[512];
|
||||
char locale_source[512];
|
||||
snprintf(renderer_link, sizeof(renderer_link), "%s/obs-webkit-renderer", temporary);
|
||||
snprintf(locale_link, sizeof(locale_link), "%s/locale", temporary);
|
||||
snprintf(locale_source, sizeof(locale_source), "%s/locale", argv[2]);
|
||||
if (symlink(argv[3], renderer_link) || symlink(locale_source, locale_link)) {
|
||||
perror("symlink");
|
||||
return 2;
|
||||
}
|
||||
if (!obs_startup("en-US", NULL, NULL)) {
|
||||
fprintf(stderr, "obs_startup failed\n");
|
||||
return 1;
|
||||
}
|
||||
obs_module_t *module = NULL;
|
||||
int result = obs_open_module(&module, argv[1], temporary);
|
||||
if (result != MODULE_SUCCESS || !obs_init_module(module)) {
|
||||
fprintf(stderr, "module load failed: %d\n", result);
|
||||
obs_shutdown();
|
||||
return 1;
|
||||
}
|
||||
const char *name = obs_source_get_display_name("webkit_browser_source");
|
||||
uint32_t flags = obs_get_source_output_flags("webkit_browser_source");
|
||||
bool valid = name && strstr(name, "WebKit") && (flags & OBS_SOURCE_ASYNC_VIDEO) &&
|
||||
(flags & OBS_SOURCE_INTERACTION);
|
||||
if (!valid)
|
||||
fprintf(stderr, "source registration is incomplete (name=%s, flags=0x%x)\n",
|
||||
name ? name : "(null)", flags);
|
||||
obs_data_t *settings = obs_data_create();
|
||||
obs_data_set_string(settings, "url", "data:text/html,<body style='background:red'></body>");
|
||||
obs_data_set_int(settings, "width", 64);
|
||||
obs_data_set_int(settings, "height", 64);
|
||||
obs_data_set_int(settings, "fps", 10);
|
||||
obs_data_set_bool(settings, "transparent", true);
|
||||
obs_data_set_bool(settings, "hardware_acceleration", false);
|
||||
obs_source_t *source = obs_source_create_private("webkit_browser_source", "smoke", settings);
|
||||
obs_data_release(settings);
|
||||
if (!source || obs_source_get_width(source) != 64 || obs_source_get_height(source) != 64) {
|
||||
fprintf(stderr, "source lifecycle setup failed\n");
|
||||
valid = false;
|
||||
}
|
||||
if (source) {
|
||||
usleep(500000);
|
||||
obs_source_release(source);
|
||||
}
|
||||
obs_shutdown();
|
||||
unlink(renderer_link);
|
||||
unlink(locale_link);
|
||||
rmdir(temporary);
|
||||
return valid ? 0 : 1;
|
||||
}
|
||||
14
tests/protocol-test.c
Normal file
14
tests/protocol-test.c
Normal file
@@ -0,0 +1,14 @@
|
||||
#include "protocol.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
assert(sizeof(struct owb_config_header) == 36);
|
||||
assert(sizeof(struct owb_command) == 100);
|
||||
assert(sizeof(struct owb_shared_frames) == 40);
|
||||
assert(offsetof(struct owb_command, text) == 36);
|
||||
assert(OWB_CONFIG_MAGIC != OWB_COMMAND_MAGIC);
|
||||
return 0;
|
||||
}
|
||||
177
tests/renderer-fps-test.c
Normal file
177
tests/renderer-fps-test.c
Normal file
@@ -0,0 +1,177 @@
|
||||
#include "protocol.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <spawn.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern char **environ;
|
||||
|
||||
static bool transfer(int fd, void *data, size_t size, bool writing)
|
||||
{
|
||||
uint8_t *cursor = data;
|
||||
while (size) {
|
||||
if (!writing) {
|
||||
struct pollfd item = {.fd = fd, .events = POLLIN};
|
||||
if (poll(&item, 1, 5000) <= 0)
|
||||
return false;
|
||||
}
|
||||
ssize_t count = writing ? write(fd, cursor, size) : 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 double seconds_since(const struct timespec *start)
|
||||
{
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (double)(now.tv_sec - start->tv_sec) + (double)(now.tv_nsec - start->tv_nsec) / 1e9;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc != 2 && argc != 5 && argc != 6)
|
||||
return 2;
|
||||
int control[2], frames[2];
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) || pipe(frames))
|
||||
return 2;
|
||||
const uint32_t width = argc > 2 ? (uint32_t)strtoul(argv[2], NULL, 10) : 1280;
|
||||
const uint32_t height = argc > 3 ? (uint32_t)strtoul(argv[3], NULL, 10) : 720;
|
||||
const uint32_t requested_fps = argc > 4 ? (uint32_t)strtoul(argv[4], NULL, 10) : 30;
|
||||
size_t shared_size = owb_shared_size(width, height);
|
||||
int shared_fd = memfd_create("renderer-fps-frames", MFD_CLOEXEC);
|
||||
if (shared_fd < 0 || ftruncate(shared_fd, (off_t)shared_size) != 0)
|
||||
return 2;
|
||||
struct owb_shared_frames *shared = mmap(NULL, shared_size, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, shared_fd, 0);
|
||||
if (shared == MAP_FAILED)
|
||||
return 2;
|
||||
memset(shared, 0, sizeof(*shared));
|
||||
shared->magic = OWB_SHARED_MAGIC;
|
||||
shared->version = OWB_PROTOCOL_VERSION;
|
||||
shared->width = width;
|
||||
shared->height = height;
|
||||
shared->stride = width * 4;
|
||||
shared->slots = OWB_SHARED_FRAME_SLOTS;
|
||||
posix_spawn_file_actions_t actions;
|
||||
posix_spawn_file_actions_init(&actions);
|
||||
posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&actions, frames[1], STDOUT_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&actions, shared_fd, OWB_SHARED_FRAME_FD);
|
||||
char *child_argv[] = {argv[1], NULL};
|
||||
pid_t child = 0;
|
||||
int error = posix_spawn(&child, argv[1], &actions, NULL, child_argv, environ);
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
close(control[1]);
|
||||
close(frames[1]);
|
||||
if (error)
|
||||
return 2;
|
||||
|
||||
const char url[] = "data:text/html,<div id='card'></div>";
|
||||
const char css_blur[] =
|
||||
"html,body{margin:0;background:transparent;overflow:hidden}"
|
||||
"body:after{content:'';position:fixed;left:0;top:0;width:32px;height:32px;"
|
||||
"animation:probe .73s linear infinite alternate}"
|
||||
"#card{position:absolute;left:15%;top:20%;width:45%;height:55%;"
|
||||
"background:rgba(30,150,255,.75);will-change:transform,filter;"
|
||||
"animation:fx 1s ease-in-out infinite alternate}"
|
||||
"@keyframes fx{from{transform:translateX(0) scale(.92);filter:blur(2px)}"
|
||||
"to{transform:translateX(45%) scale(1.08);filter:blur(18px)}}"
|
||||
"@keyframes probe{from{background:#000}to{background:#fff}}";
|
||||
const char css_plain[] =
|
||||
"html,body{margin:0;background:transparent;overflow:hidden}"
|
||||
"body:after{content:'';position:fixed;left:0;top:0;width:32px;height:32px;"
|
||||
"animation:probe .73s linear infinite alternate}"
|
||||
"#card{position:absolute;left:15%;top:20%;width:45%;height:55%;"
|
||||
"background:rgba(30,150,255,.75);animation:fx 1s ease-in-out infinite alternate}"
|
||||
"@keyframes fx{from{transform:translateX(0) scale(.92)}"
|
||||
"to{transform:translateX(45%) scale(1.08)}}"
|
||||
"@keyframes probe{from{background:#000}to{background:#fff}}";
|
||||
const char *css = argc == 6 && strstr(argv[5], "plain") != NULL ? css_plain : css_blur;
|
||||
size_t css_length = strlen(css);
|
||||
struct owb_config_header config = {
|
||||
.magic = OWB_CONFIG_MAGIC, .version = OWB_PROTOCOL_VERSION,
|
||||
.width = width, .height = height, .fps = requested_fps, .transparent = 1,
|
||||
.hardware_acceleration = 1,
|
||||
.url_length = sizeof(url) - 1, .css_length = (uint32_t)css_length,
|
||||
};
|
||||
bool okay = transfer(control[0], &config, sizeof(config), true) &&
|
||||
transfer(control[0], (void *)url, sizeof(url) - 1, true) &&
|
||||
transfer(control[0], (void *)css, css_length, true);
|
||||
size_t frame_size = (size_t)width * height * 4;
|
||||
uint8_t *pixels = malloc(frame_size);
|
||||
unsigned frames_read = 0;
|
||||
unsigned changed_frames = 0;
|
||||
uint64_t previous_hash = 0;
|
||||
struct timespec start = {0};
|
||||
while (okay) {
|
||||
uint8_t slot;
|
||||
if (!transfer(frames[0], &slot, sizeof(slot), false) || slot >= OWB_SHARED_FRAME_SLOTS)
|
||||
break;
|
||||
bool stable = false;
|
||||
for (unsigned attempt = 0; attempt < 3 && !stable; attempt++) {
|
||||
uint64_t before = __atomic_load_n(&shared->sequence[slot], __ATOMIC_ACQUIRE);
|
||||
if (before & 1u)
|
||||
continue;
|
||||
memcpy(pixels, owb_shared_slot(shared, slot), frame_size);
|
||||
uint64_t after = __atomic_load_n(&shared->sequence[slot], __ATOMIC_ACQUIRE);
|
||||
stable = before == after && !(after & 1u);
|
||||
}
|
||||
if (!stable)
|
||||
continue;
|
||||
uint64_t hash = 1469598103934665603ull;
|
||||
for (uint32_t y = 0; y < height; y += 16) {
|
||||
for (uint32_t x = 0; x < width; x += 16) {
|
||||
size_t offset = ((size_t)y * width + x) * 4;
|
||||
uint32_t sample;
|
||||
memcpy(&sample, pixels + offset, sizeof(sample));
|
||||
hash ^= sample;
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
}
|
||||
if (!frames_read || hash != previous_hash)
|
||||
changed_frames++;
|
||||
previous_hash = hash;
|
||||
if (!frames_read)
|
||||
clock_gettime(CLOCK_MONOTONIC, &start);
|
||||
frames_read++;
|
||||
if (seconds_since(&start) >= 4.0)
|
||||
break;
|
||||
}
|
||||
double elapsed = frames_read ? seconds_since(&start) : 0.0;
|
||||
double fps = elapsed > 0.0 ? (double)(frames_read - 1) / elapsed : 0.0;
|
||||
double changed_fps = elapsed > 0.0 ? (double)(changed_frames - 1) / elapsed : 0.0;
|
||||
fprintf(stderr,
|
||||
"blur-transition: %u frames in %.3f s = %.2f FPS; %u changed = %.2f FPS "
|
||||
"(requested %u)\n",
|
||||
frames_read, elapsed, fps, changed_frames, changed_fps, requested_fps);
|
||||
free(pixels);
|
||||
munmap(shared, shared_size);
|
||||
close(shared_fd);
|
||||
shutdown(control[0], SHUT_RDWR);
|
||||
close(control[0]);
|
||||
close(frames[0]);
|
||||
kill(child, SIGTERM);
|
||||
while (waitpid(child, NULL, 0) < 0 && errno == EINTR) {
|
||||
}
|
||||
return frames_read && fps >= requested_fps * 0.80 && changed_fps >= requested_fps * 0.50
|
||||
? 0
|
||||
: 1;
|
||||
}
|
||||
192
tests/renderer-smoke-test.c
Normal file
192
tests/renderer-smoke-test.c
Normal file
@@ -0,0 +1,192 @@
|
||||
#include "protocol.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <spawn.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern char **environ;
|
||||
|
||||
static bool write_exact(int fd, const void *data, size_t size)
|
||||
{
|
||||
const uint8_t *cursor = data;
|
||||
while (size) {
|
||||
ssize_t count = write(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_frame(int fd, uint8_t *pixels, size_t size, int timeout_ms)
|
||||
{
|
||||
size_t used = 0;
|
||||
while (used < size) {
|
||||
struct pollfd pollfd = {.fd = fd, .events = POLLIN};
|
||||
int ready = poll(&pollfd, 1, timeout_ms);
|
||||
if (ready <= 0)
|
||||
return false;
|
||||
ssize_t count = read(fd, pixels + used, size - used);
|
||||
if (count < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (count <= 0)
|
||||
return false;
|
||||
used += (size_t)count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2 || argc > 4) {
|
||||
fprintf(stderr,
|
||||
"usage: %s /path/to/obs-webkit-renderer [opaque | opaque-url URL]\n",
|
||||
argv[0]);
|
||||
return 2;
|
||||
}
|
||||
bool external_url = argc == 4 && strcmp(argv[2], "opaque-url") == 0;
|
||||
bool opaque = external_url || (argc == 3 && strcmp(argv[2], "opaque") == 0);
|
||||
int control[2];
|
||||
int frames[2];
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) || pipe(frames)) {
|
||||
perror("IPC");
|
||||
return 2;
|
||||
}
|
||||
const uint32_t width = external_url ? 1890 : 64;
|
||||
const uint32_t height = external_url ? 660 : 64;
|
||||
size_t shared_size = owb_shared_size(width, height);
|
||||
int shared_fd = memfd_create("renderer-smoke-frames", MFD_CLOEXEC);
|
||||
if (shared_fd < 0 || ftruncate(shared_fd, (off_t)shared_size) != 0)
|
||||
return 2;
|
||||
struct owb_shared_frames *shared = mmap(NULL, shared_size, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, shared_fd, 0);
|
||||
if (shared == MAP_FAILED)
|
||||
return 2;
|
||||
memset(shared, 0, sizeof(*shared));
|
||||
shared->magic = OWB_SHARED_MAGIC;
|
||||
shared->version = OWB_PROTOCOL_VERSION;
|
||||
shared->width = width;
|
||||
shared->height = height;
|
||||
shared->stride = width * 4;
|
||||
shared->slots = OWB_SHARED_FRAME_SLOTS;
|
||||
posix_spawn_file_actions_t actions;
|
||||
posix_spawn_file_actions_init(&actions);
|
||||
posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&actions, frames[1], STDOUT_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&actions, shared_fd, OWB_SHARED_FRAME_FD);
|
||||
char *child_argv[] = {argv[1], NULL};
|
||||
pid_t child = 0;
|
||||
int error = posix_spawn(&child, argv[1], &actions, NULL, child_argv, environ);
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
close(control[1]);
|
||||
close(frames[1]);
|
||||
if (error) {
|
||||
fprintf(stderr, "posix_spawn: %s\n", strerror(error));
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char internal_url[] = "data:text/html,<body><div id='box'></div></body>";
|
||||
const char transparent_css[] = "html,body{margin:0;background:transparent!important}"
|
||||
"#box{position:fixed;inset:0;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)}";
|
||||
const char *url = external_url ? argv[3] : internal_url;
|
||||
const char *css = external_url ? "" : opaque ? opaque_css : transparent_css;
|
||||
struct owb_config_header config = {
|
||||
.magic = OWB_CONFIG_MAGIC,
|
||||
.version = OWB_PROTOCOL_VERSION,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.fps = 10,
|
||||
.transparent = opaque ? 0 : 1,
|
||||
/* GPU rendering must still use the private Xvfb and produce a frame. */
|
||||
.hardware_acceleration = 1,
|
||||
.url_length = (uint32_t)strlen(url),
|
||||
.css_length = (uint32_t)strlen(css),
|
||||
};
|
||||
bool sent = write_exact(control[0], &config, sizeof(config)) &&
|
||||
write_exact(control[0], url, strlen(url)) &&
|
||||
write_exact(control[0], css, strlen(css));
|
||||
uint8_t last_bgra[4] = {0};
|
||||
bool found_red = false;
|
||||
int max_attempts = external_url ? 120 : 60;
|
||||
for (int attempt = 0; sent && attempt < max_attempts; attempt++) {
|
||||
uint8_t slot;
|
||||
if (!read_frame(frames[0], &slot, sizeof(slot), 1000))
|
||||
continue;
|
||||
if (slot >= OWB_SHARED_FRAME_SLOTS)
|
||||
continue;
|
||||
uint8_t *frame = owb_shared_slot(shared, slot);
|
||||
if (external_url) {
|
||||
bool nonuniform = false;
|
||||
uint32_t first;
|
||||
memcpy(&first, frame, sizeof(first));
|
||||
for (uint32_t y = 0; y < height && !nonuniform; y += 8) {
|
||||
for (uint32_t x = 0; x < width; x += 8) {
|
||||
uint32_t sample;
|
||||
memcpy(&sample, frame + ((size_t)y * width + x) * 4,
|
||||
sizeof(sample));
|
||||
if (sample != first) {
|
||||
nonuniform = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
found_red = nonuniform;
|
||||
if ((attempt + 1) % 10 == 0)
|
||||
fprintf(stderr, "external page at %.1fs: %s\n",
|
||||
(double)(attempt + 1) / 10.0,
|
||||
nonuniform ? "content" : "uniform");
|
||||
continue;
|
||||
}
|
||||
size_t center = ((size_t)(height / 2) * width + width / 2) * 4;
|
||||
memcpy(last_bgra, frame + center, sizeof(last_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;
|
||||
if (expected) {
|
||||
found_red = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
shutdown(control[0], SHUT_RDWR);
|
||||
close(control[0]);
|
||||
close(frames[0]);
|
||||
munmap(shared, shared_size);
|
||||
close(shared_fd);
|
||||
kill(child, SIGTERM);
|
||||
while (waitpid(child, NULL, 0) < 0 && errno == EINTR) {
|
||||
}
|
||||
if (!found_red) {
|
||||
if (external_url) {
|
||||
fprintf(stderr, "renderer kept the external page uniformly colored: %s\n", url);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr,
|
||||
"renderer did not produce the expected %s red BGRA frame "
|
||||
"(center BGRA=%u,%u,%u,%u)\n",
|
||||
opaque ? "opaque" : "transparent", last_bgra[0], last_bgra[1],
|
||||
last_bgra[2], last_bgra[3]);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user