Compare commits

...

4 Commits

Author SHA1 Message Date
53161f3f2c update build 2026-07-18 12:30:55 +05:00
8b386b3ab2 add sync tabs + refresh button 2026-07-18 12:09:17 +05:00
3652cfab3c add icon 2026-07-18 11:36:44 +05:00
d3872d60ef web docks 2026-07-18 11:33:47 +05:00
15 changed files with 1683 additions and 111 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,6 @@
/build/ /build/
/build-*/
/dist/
*.o *.o
*.so *.so
*.swp *.swp

View File

@@ -1,12 +1,15 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(obs-webkit-browser VERSION 0.1.0 LANGUAGES C) project(obs-webkit-browser VERSION 1.1.0 LANGUAGES C CXX)
include(GNUInstallDirs) include(GNUInstallDirs)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
pkg_check_modules(OBS REQUIRED IMPORTED_TARGET libobs) pkg_check_modules(OBS REQUIRED IMPORTED_TARGET libobs)
pkg_check_modules(OBS_FRONTEND REQUIRED IMPORTED_TARGET obs-frontend-api)
pkg_check_modules(QT_WIDGETS REQUIRED IMPORTED_TARGET Qt6Widgets)
find_path(OBS_HEADER_DIRECTORY obs.h PATH_SUFFIXES obs REQUIRED)
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)
@@ -14,12 +17,20 @@ pkg_check_modules(X11_CAPTURE REQUIRED IMPORTED_TARGET x11 xcomposite xdamage xe
add_library(obs-webkit-browser MODULE add_library(obs-webkit-browser MODULE
src/plugin.c src/plugin.c
src/docks.cpp
src/protocol.h src/protocol.h
) )
target_compile_features(obs-webkit-browser PRIVATE c_std_11) target_compile_features(obs-webkit-browser PRIVATE c_std_11)
target_compile_features(obs-webkit-browser PRIVATE cxx_std_17)
target_compile_definitions(obs-webkit-browser PRIVATE _GNU_SOURCE) target_compile_definitions(obs-webkit-browser PRIVATE _GNU_SOURCE)
target_compile_options(obs-webkit-browser PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(obs-webkit-browser PRIVATE -Wall -Wextra -Wpedantic)
target_link_libraries(obs-webkit-browser PRIVATE PkgConfig::OBS Threads::Threads) target_include_directories(obs-webkit-browser PRIVATE "${OBS_HEADER_DIRECTORY}")
target_link_libraries(obs-webkit-browser PRIVATE
obs
obs-frontend-api
PkgConfig::QT_WIDGETS
Threads::Threads
)
set_target_properties(obs-webkit-browser PROPERTIES PREFIX "") set_target_properties(obs-webkit-browser PROPERTIES PREFIX "")
add_executable(obs-webkit-renderer add_executable(obs-webkit-renderer
@@ -76,6 +87,14 @@ if(BUILD_TESTING)
COMMAND renderer-fps-test $<TARGET_FILE:obs-webkit-renderer>) COMMAND renderer-fps-test $<TARGET_FILE:obs-webkit-renderer>)
set_tests_properties(blur-transition-fps-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) set_tests_properties(blur-transition-fps-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
add_executable(shared-profile-test tests/shared-profile-test.c)
target_compile_features(shared-profile-test PRIVATE c_std_11)
target_compile_definitions(shared-profile-test PRIVATE _GNU_SOURCE)
target_include_directories(shared-profile-test PRIVATE src)
add_test(NAME shared-profile-test
COMMAND shared-profile-test $<TARGET_FILE:obs-webkit-renderer>)
set_tests_properties(shared-profile-test PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77)
add_executable(module-load-test tests/module-load-test.c) add_executable(module-load-test tests/module-load-test.c)
target_compile_features(module-load-test PRIVATE c_std_11) target_compile_features(module-load-test PRIVATE c_std_11)
target_link_libraries(module-load-test PRIVATE PkgConfig::OBS) target_link_libraries(module-load-test PRIVATE PkgConfig::OBS)

57
Makefile Normal file
View File

@@ -0,0 +1,57 @@
PROJECT_NAME := obs-webkit-browser
VERSION := $(shell sed -n '/^project.* VERSION / { s/^project.* VERSION //; s/ .*//; p; }' CMakeLists.txt)
ARCH := $(shell uname -m)
BUILD_DIR ?= build-release
DIST_DIR ?= dist
STAGE_DIR ?= $(BUILD_DIR)/stage
INSTALL_PREFIX ?= /usr
ARCHIVE_NAME := $(PROJECT_NAME)-$(VERSION)-linux-$(ARCH).tar.gz
ARCHIVE_PATH := $(abspath $(DIST_DIR)/$(ARCHIVE_NAME))
PLUGIN_STAGE_DIR := $(STAGE_DIR)/$(PROJECT_NAME)
PLUGIN_BIN_DIR := $(PLUGIN_STAGE_DIR)/bin/64bit
PLUGIN_DATA_DIR := $(PLUGIN_STAGE_DIR)/data
CMAKE ?= $(if $(wildcard /usr/bin/cmake),/usr/bin/cmake,cmake)
CTEST ?= $(if $(wildcard /usr/bin/ctest),/usr/bin/ctest,ctest)
.PHONY: all configure build test install stage archive clean
all: build
configure:
$(CMAKE) -S . -B $(BUILD_DIR) -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$(INSTALL_PREFIX)
build: configure
$(CMAKE) --build $(BUILD_DIR)
test: build
$(CTEST) --test-dir $(BUILD_DIR) --output-on-failure
install: build
$(CMAKE) --install $(BUILD_DIR) --strip
stage: test
$(CMAKE) -E remove_directory $(STAGE_DIR)
$(CMAKE) -E make_directory $(PLUGIN_BIN_DIR)
$(CMAKE) -E make_directory $(PLUGIN_DATA_DIR)/locale
$(CMAKE) -E copy_if_different \
$(BUILD_DIR)/obs-webkit-browser.so $(PLUGIN_BIN_DIR)/obs-webkit-browser.so
$(CMAKE) -E copy_if_different \
$(BUILD_DIR)/obs-webkit-renderer $(PLUGIN_DATA_DIR)/obs-webkit-renderer
$(CMAKE) -E copy_if_different \
data/locale/en-US.ini data/locale/ru-RU.ini $(PLUGIN_DATA_DIR)/locale/
archive: stage
$(CMAKE) -E make_directory $(DIST_DIR)
cd $(STAGE_DIR) && $(CMAKE) -E tar cfvz $(ARCHIVE_PATH) --format=gnutar $(PROJECT_NAME)
$(CMAKE) -E sha256sum $(ARCHIVE_PATH) >$(ARCHIVE_PATH).sha256
@echo
@echo "Archive: $(ARCHIVE_PATH)"
@echo "SHA-256: $(ARCHIVE_PATH).sha256"
clean:
$(CMAKE) -E remove_directory $(BUILD_DIR)
$(CMAKE) -E remove_directory $(DIST_DIR)

View File

@@ -19,8 +19,18 @@ DMA-BUF. Кадры передаются в OBS напрямую как BGRA, б
- shared-memory транспорт кадров без многомегабайтной записи в pipe; - shared-memory транспорт кадров без многомегабайтной записи в pipe;
- мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS; - мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS;
- перезагрузка страницы и остановка renderer-процесса у скрытого source. - перезагрузка страницы и остановка renderer-процесса у скрытого source.
- пользовательские web-панели в меню «Док-панели» OBS с сохранением URL в
коллекции сцен, восстановлением после запуска и интерактивным вводом.
- единый постоянный WebKit-профиль для источников на сцене и web-панелей: общие
cookies, HTTP cache, localStorage, IndexedDB, service workers и credentials;
страницы одного origin взаимодействуют штатными `storage`-событиями и
`BroadcastChannel`, как вкладки одного браузера.
Звук страницы намеренно выключен: добавьте в OBS отдельный Application Audio Capture. Звук страницы намеренно выключен: добавьте в OBS отдельный Application Audio Capture.
Все WebView одной сессии OBS работают в общем renderer-процессе и одном
`WebKitWebContext`; маленькие процессы-посредники сохраняют прежнюю независимую
жизнь каждого source/dock и изоляцию от OBS. Профиль хранится в стандартных каталогах
данных и cache пользователя (`obs-webkit-browser/profile`) и переживает перезапуск OBS.
Renderer наследует X11/XWayland-дисплей OBS; его нативное окно имеет нулевую opacity, Renderer наследует X11/XWayland-дисплей OBS; его нативное окно имеет нулевую opacity,
пустую input shape и не появляется на рабочем столе. Дополнительный display server не пустую input shape и не появляется на рабочем столе. Дополнительный display server не
запускается. WebKit принудительно включает аппаратный compositor и выбирает доступный запускается. WebKit принудительно включает аппаратный compositor и выбирает доступный
@@ -31,7 +41,7 @@ DRM render node; для него нужен доступ к `/dev/dri/renderD*`.
WebKit сам распределяет элементы по GPU-слоям. WebKit сам распределяет элементы по GPU-слоям.
Это рабочий backend, а не замена всех расширенных возможностей штатного obs-browser Это рабочий backend, а не замена всех расширенных возможностей штатного obs-browser
(DevTools, cookies API, browser docks и DRM здесь пока отсутствуют). (DevTools, cookies API и DRM здесь пока отсутствуют).
## Сборка на Alpine ## Сборка на Alpine
@@ -44,10 +54,39 @@ ctest --test-dir build --output-on-failure
doas /usr/bin/cmake --install build --prefix /usr doas /usr/bin/cmake --install build --prefix /usr
``` ```
После установки перезапустите OBS и добавьте источник **Браузер WebKit (без CEF)**. После установки перезапустите OBS и добавьте источник **Браузер WebKit**.
Для панели откройте **Док-панели → Пользовательские web-панели...**, добавьте
название и URL и нажмите **Применить**. Панель можно скрывать и показывать через
обычное меню доков OBS; правый клик по странице открывает команду перезагрузки.
Если OBS установлен в нестандартный prefix, задайте при конфигурации Если OBS установлен в нестандартный prefix, задайте при конфигурации
`OBS_PLUGIN_DIR` и `OBS_PLUGIN_DATA_DIR`. `OBS_PLUGIN_DIR` и `OBS_PLUGIN_DATA_DIR`.
## Архив для публикации
После установки зависимостей используйте отдельные цели `Makefile`:
```sh
make build
make test
doas make install
make archive
```
`make build` собирает Release-версию, `make test` запускает тесты,
`doas make install` устанавливает плагин в `/usr`, а `make archive` запускает
сборку и тесты, после чего создаёт пакет без установки файлов в систему.
Готовый архив `obs-webkit-browser-<version>-linux-<arch>.tar.gz` и файл с его
SHA-256 будут записаны в каталог `dist/`. Внутри нет абсолютных путей: архив
содержит только пользовательскую структуру плагина
`obs-webkit-browser/{bin/64bit,data}`. Установить её можно напрямую в каталог
пользовательских плагинов OBS:
```sh
mkdir -p ~/.config/obs-studio/plugins
tar -xzf dist/obs-webkit-browser-<version>-linux-<arch>.tar.gz \
-C ~/.config/obs-studio/plugins
```
## Почему WebKitGTK, а не WPE ## Почему WebKitGTK, а не WPE
WPE архитектурно подходит лучше всего, но пакет `wpewebkit` отсутствует в актуальном WPE архитектурно подходит лучше всего, но пакет `wpewebkit` отсутствует в актуальном

View File

@@ -1,4 +1,14 @@
WebKitBrowserSource="WebKit Browser (no CEF)" WebKitBrowserSource="WebKit Browser"
CustomBrowserDocks="Custom Web Panels..."
CustomBrowserDocksDescription="Add web panels to the OBS Docks menu. Each panel is rendered by WebKitGTK."
PanelName="Panel name"
AddPanel="Add"
RemovePanel="Remove"
PanelFieldsRequired="Every panel must have a name and URL."
PanelNamesUnique="Panel names must be unique."
PanelLoading="Loading web panel..."
PanelRendererError="Unable to start the WebKit panel renderer."
PanelRendererMissing="The WebKit renderer helper is not installed."
URL="URL or local HTML file" URL="URL or local HTML file"
Width="Width" Width="Width"
Height="Height" Height="Height"
@@ -7,3 +17,4 @@ Transparent="Transparent background"
CustomCSS="Custom CSS" CustomCSS="Custom CSS"
ShutdownWhenHidden="Shut down renderer when source is hidden" ShutdownWhenHidden="Shut down renderer when source is hidden"
Reload="Reload page" Reload="Reload page"
Refresh="Refresh"

View File

@@ -1,4 +1,14 @@
WebKitBrowserSource="Браузер WebKit (без CEF)" WebKitBrowserSource="Браузер WebKit"
CustomBrowserDocks="Пользовательские web-панели..."
CustomBrowserDocksDescription="Добавьте web-панели в меню «Док-панели» OBS. Страницы рендерятся через WebKitGTK."
PanelName="Название панели"
AddPanel="Добавить"
RemovePanel="Удалить"
PanelFieldsRequired="Для каждой панели укажите название и URL."
PanelNamesUnique="Названия панелей не должны повторяться."
PanelLoading="Загрузка web-панели..."
PanelRendererError="Не удалось запустить WebKit-рендерер панели."
PanelRendererMissing="Вспомогательный WebKit-рендерер не установлен."
URL="URL или локальный HTML-файл" URL="URL или локальный HTML-файл"
Width="Ширина" Width="Ширина"
Height="Высота" Height="Высота"
@@ -7,3 +17,4 @@ Transparent="Прозрачный фон"
CustomCSS="Пользовательский CSS" CustomCSS="Пользовательский CSS"
ShutdownWhenHidden="Останавливать рендерер, когда источник скрыт" ShutdownWhenHidden="Останавливать рендерер, когда источник скрыт"
Reload="Перезагрузить страницу" Reload="Перезагрузить страницу"
Refresh="Обновить"

View File

@@ -15,6 +15,7 @@ fi
$ROOT apk add \ $ROOT apk add \
build-base cmake samurai pkgconf \ build-base cmake samurai pkgconf \
obs-studio obs-studio-dev \ obs-studio obs-studio-dev \
qt6-qtbase-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 libxdamage-dev libxext-dev \ libx11-dev libxcomposite-dev libxdamage-dev libxext-dev \

903
src/docks.cpp Normal file
View File

@@ -0,0 +1,903 @@
#include <obs/obs-frontend-api.h>
#include <obs/obs-interaction.h>
#include <obs/obs-module.h>
#include <QAbstractItemView>
#include <QAction>
#include <QApplication>
#include <QByteArray>
#include <QBoxLayout>
#include <QContextMenuEvent>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDockWidget>
#include <QFocusEvent>
#include <QHash>
#include <QHeaderView>
#include <QImage>
#include <QKeyEvent>
#include <QLabel>
#include <QMainWindow>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QPointer>
#include <QPushButton>
#include <QResizeEvent>
#include <QSet>
#include <QShowEvent>
#include <QSocketNotifier>
#include <QTableWidget>
#include <QTimer>
#include <QUuid>
#include <QVBoxLayout>
#include <QVector>
#include <QWheelEvent>
#include <QWidget>
#include <algorithm>
#include <cerrno>
#include <csignal>
#include <cstdint>
#include <cstring>
#include <fcntl.h>
#include <spawn.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <utility>
#include "protocol.h"
extern "C" {
extern char **environ;
const char *owb_module_text(const char *key);
}
namespace {
constexpr const char *kSaveKey = "obs-webkit-browser-docks";
constexpr const char *kDockPrefix = "obs-webkit-browser-dock-";
QString text(const char *key)
{
return QString::fromUtf8(owb_module_text(key));
}
bool writeAll(int fd, const void *data, size_t size)
{
auto cursor = static_cast<const uint8_t *>(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 += written;
size -= static_cast<size_t>(written);
}
return true;
}
void closeFd(int &fd)
{
if (fd >= 0)
close(fd);
fd = -1;
}
void closeOnExec(int fd)
{
int flags = fcntl(fd, F_GETFD);
if (flags >= 0)
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
}
void nonBlocking(int fd)
{
int flags = fcntl(fd, F_GETFL);
if (flags >= 0)
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
struct DockDefinition {
QString id;
QString title;
QString url;
};
class WebPanel final : public QWidget {
public:
explicit WebPanel(QString url, QWidget *parent = nullptr) : QWidget(parent), url_(std::move(url))
{
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
setMinimumSize(80, 60);
resizeTimer_.setSingleShot(true);
resizeTimer_.setInterval(250);
connect(&resizeTimer_, &QTimer::timeout, this, [this] {
if (isVisible())
restartRenderer();
});
}
~WebPanel() override { stopRenderer(); }
void setUrl(const QString &url)
{
if (url_ == url)
return;
url_ = url;
if (isVisible())
restartRenderer();
}
void setDockTitle(const QString &title)
{
for (QWidget *ancestor = parentWidget(); ancestor; ancestor = ancestor->parentWidget()) {
if (auto *dock = qobject_cast<QDockWidget *>(ancestor)) {
dock->setWindowTitle(title);
break;
}
}
}
protected:
bool event(QEvent *event) override
{
const auto dockClosed = static_cast<QEvent::Type>(QEvent::User + QEvent::Close);
if (event->type() == dockClosed)
stopRenderer();
return QWidget::event(event);
}
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
painter.fillRect(rect(), palette().color(QPalette::Base));
if (!frame_.isNull()) {
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
painter.drawImage(rect(), frame_);
} else if (!status_.isEmpty()) {
painter.setPen(palette().color(QPalette::Text));
painter.drawText(rect().adjusted(16, 16, -16, -16), Qt::AlignCenter | Qt::TextWordWrap,
status_);
}
}
void showEvent(QShowEvent *event) override
{
QWidget::showEvent(event);
if (pid_ <= 0)
QTimer::singleShot(0, this, [this] { startRenderer(); });
}
void hideEvent(QHideEvent *event) override
{
resizeTimer_.stop();
stopRenderer();
QWidget::hideEvent(event);
}
void resizeEvent(QResizeEvent *event) override
{
QWidget::resizeEvent(event);
if (pid_ > 0)
resizeTimer_.start();
}
void mouseMoveEvent(QMouseEvent *event) override
{
auto [x, y] = rendererPosition(event->position());
owb_command command{};
command.type = OWB_MOUSE_MOVE;
command.x = x;
command.y = y;
command.modifiers = interactionModifiers(event->modifiers(), event->buttons());
sendCommand(command);
}
void leaveEvent(QEvent *event) override
{
owb_command command{};
command.type = OWB_MOUSE_MOVE;
command.value1 = 1;
sendCommand(command);
QWidget::leaveEvent(event);
}
void mousePressEvent(QMouseEvent *event) override
{
setFocus(Qt::MouseFocusReason);
sendMouseButton(event, false);
}
void mouseReleaseEvent(QMouseEvent *event) override { sendMouseButton(event, true); }
void wheelEvent(QWheelEvent *event) override
{
auto [x, y] = rendererPosition(event->position());
owb_command command{};
command.type = OWB_MOUSE_WHEEL;
command.x = x;
command.y = y;
command.value1 = event->angleDelta().x();
command.value2 = event->angleDelta().y();
command.modifiers = interactionModifiers(event->modifiers(), event->buttons());
sendCommand(command);
event->accept();
}
void focusInEvent(QFocusEvent *event) override
{
owb_command command{};
command.type = OWB_FOCUS;
command.value1 = 1;
sendCommand(command);
QWidget::focusInEvent(event);
}
void focusOutEvent(QFocusEvent *event) override
{
owb_command command{};
command.type = OWB_FOCUS;
sendCommand(command);
QWidget::focusOutEvent(event);
}
void keyPressEvent(QKeyEvent *event) override { sendKey(event, false); }
void keyReleaseEvent(QKeyEvent *event) override { sendKey(event, true); }
void contextMenuEvent(QContextMenuEvent *event) override
{
QMenu menu(this);
QAction *reload = menu.addAction(text("Reload"));
if (menu.exec(event->globalPos()) == reload) {
owb_command command{};
command.type = OWB_RELOAD;
sendCommand(command);
}
}
private:
std::pair<int32_t, int32_t> rendererPosition(const QPointF &position) const
{
const double sx = width() > 0 ? static_cast<double>(renderWidth_) / width() : 1.0;
const double sy = height() > 0 ? static_cast<double>(renderHeight_) / height() : 1.0;
int x = std::clamp(static_cast<int>(position.x() * sx), 0, std::max(0, renderWidth_ - 1));
int y = std::clamp(static_cast<int>(position.y() * sy), 0, std::max(0, renderHeight_ - 1));
return {x, y};
}
static uint32_t interactionModifiers(Qt::KeyboardModifiers keys, Qt::MouseButtons buttons)
{
uint32_t result = 0;
if (keys & Qt::ShiftModifier)
result |= INTERACT_SHIFT_KEY;
if (keys & Qt::ControlModifier)
result |= INTERACT_CONTROL_KEY;
if (keys & Qt::AltModifier)
result |= INTERACT_ALT_KEY;
if (keys & Qt::MetaModifier)
result |= INTERACT_COMMAND_KEY;
if (buttons & Qt::LeftButton)
result |= INTERACT_MOUSE_LEFT;
if (buttons & Qt::MiddleButton)
result |= INTERACT_MOUSE_MIDDLE;
if (buttons & Qt::RightButton)
result |= INTERACT_MOUSE_RIGHT;
return result;
}
void sendMouseButton(QMouseEvent *event, bool released)
{
auto [x, y] = rendererPosition(event->position());
owb_command command{};
command.type = OWB_MOUSE_CLICK;
command.x = x;
command.y = y;
command.value1 = event->button() == Qt::MiddleButton ? MOUSE_MIDDLE
: event->button() == Qt::RightButton ? MOUSE_RIGHT
: MOUSE_LEFT;
command.value2 = released;
command.text_length = static_cast<uint32_t>(event->type() == QEvent::MouseButtonDblClick ? 2 : 1);
command.modifiers = interactionModifiers(event->modifiers(), event->buttons());
sendCommand(command);
event->accept();
}
void sendKey(QKeyEvent *event, bool released)
{
owb_command command{};
command.type = OWB_KEY;
command.value1 = released;
command.value2 = static_cast<int32_t>(event->nativeVirtualKey());
command.modifiers = interactionModifiers(event->modifiers(), {});
QByteArray utf8 = event->text().toUtf8();
command.text_length = static_cast<uint32_t>(std::min<size_t>(utf8.size(), sizeof(command.text)));
memcpy(command.text, utf8.constData(), command.text_length);
sendCommand(command);
event->accept();
}
void sendCommand(owb_command &command)
{
if (controlFd_ < 0)
return;
command.magic = OWB_COMMAND_MAGIC;
writeAll(controlFd_, &command, sizeof(command));
}
bool sendConfiguration()
{
QByteArray url = url_.trimmed().toUtf8();
if (url.isEmpty())
url = "about:blank";
owb_config_header header{};
header.magic = OWB_CONFIG_MAGIC;
header.version = OWB_PROTOCOL_VERSION;
header.width = static_cast<uint32_t>(renderWidth_);
header.height = static_cast<uint32_t>(renderHeight_);
header.fps = 30;
header.transparent = 0;
header.hardware_acceleration = 1;
header.url_length = static_cast<uint32_t>(url.size());
return writeAll(controlFd_, &header, sizeof(header)) &&
writeAll(controlFd_, url.constData(), static_cast<size_t>(url.size()));
}
void startRenderer()
{
if (pid_ > 0 || !isVisible())
return;
status_ = text("PanelLoading");
frame_ = QImage();
update();
const double scale = devicePixelRatioF();
renderWidth_ = std::clamp(static_cast<int>(width() * scale), 2, 8192);
renderHeight_ = std::clamp(static_cast<int>(height() * scale), 2, 8192);
sharedSize_ = owb_shared_size(renderWidth_, renderHeight_);
sharedFd_ = memfd_create("obs-webkit-browser-dock", MFD_CLOEXEC);
if (sharedFd_ < 0 || ftruncate(sharedFd_, static_cast<off_t>(sharedSize_)) != 0)
return startFailed(text("PanelRendererError"));
shared_ = static_cast<owb_shared_frames *>(
mmap(nullptr, sharedSize_, PROT_READ | PROT_WRITE, MAP_SHARED, sharedFd_, 0));
if (shared_ == MAP_FAILED) {
shared_ = nullptr;
return startFailed(text("PanelRendererError"));
}
memset(shared_, 0, sizeof(*shared_));
shared_->magic = OWB_SHARED_MAGIC;
shared_->version = OWB_PROTOCOL_VERSION;
shared_->width = static_cast<uint32_t>(renderWidth_);
shared_->height = static_cast<uint32_t>(renderHeight_);
shared_->stride = static_cast<uint32_t>(renderWidth_ * 4);
shared_->slot_count = OWB_SHARED_FRAME_SLOTS;
int control[2] = {-1, -1};
int frames[2] = {-1, -1};
int logs[2] = {-1, -1};
if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) != 0 || pipe(frames) != 0 || pipe(logs) != 0) {
for (int fd : control)
if (fd >= 0)
::close(fd);
for (int fd : frames)
if (fd >= 0)
::close(fd);
for (int fd : logs)
if (fd >= 0)
::close(fd);
return startFailed(text("PanelRendererError"));
}
for (int fd : control)
closeOnExec(fd);
for (int fd : frames)
closeOnExec(fd);
for (int fd : logs)
closeOnExec(fd);
char *helper = obs_module_file("obs-webkit-renderer");
if (!helper || access(helper, X_OK) != 0) {
for (int fd : control)
::close(fd);
for (int fd : frames)
::close(fd);
for (int fd : logs)
::close(fd);
bfree(helper);
return startFailed(text("PanelRendererMissing"));
}
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, sharedFd_, 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, nullptr};
pid_t child = 0;
int spawnError = posix_spawn(&child, helper, &actions, &attributes, argv, environ);
posix_spawn_file_actions_destroy(&actions);
posix_spawnattr_destroy(&attributes);
bfree(helper);
::close(control[1]);
::close(frames[1]);
::close(logs[1]);
if (spawnError != 0) {
::close(control[0]);
::close(frames[0]);
::close(logs[0]);
return startFailed(text("PanelRendererError"));
}
pid_ = child;
controlFd_ = control[0];
frameFd_ = frames[0];
logFd_ = logs[0];
nonBlocking(frameFd_);
nonBlocking(logFd_);
if (!sendConfiguration()) {
stopRenderer();
return startFailed(text("PanelRendererError"));
}
frameNotifier_ = new QSocketNotifier(frameFd_, QSocketNotifier::Read, this);
connect(frameNotifier_, &QSocketNotifier::activated, this, [this] { readFrames(); });
logNotifier_ = new QSocketNotifier(logFd_, QSocketNotifier::Read, this);
connect(logNotifier_, &QSocketNotifier::activated, this, [this] { readLogs(); });
}
void startFailed(const QString &message)
{
status_ = message;
stopRenderer();
update();
}
void restartRenderer()
{
stopRenderer();
startRenderer();
}
void stopRenderer()
{
delete frameNotifier_;
frameNotifier_ = nullptr;
delete logNotifier_;
logNotifier_ = nullptr;
if (controlFd_ >= 0)
shutdown(controlFd_, SHUT_RDWR);
closeFd(controlFd_);
closeFd(frameFd_);
closeFd(logFd_);
if (pid_ > 0) {
kill(-pid_, SIGTERM);
for (unsigned attempt = 0; attempt < 50; ++attempt) {
pid_t result = waitpid(pid_, nullptr, WNOHANG);
if (result == pid_ || (result < 0 && errno == ECHILD)) {
pid_ = 0;
break;
}
usleep(10000);
}
if (pid_ > 0) {
kill(-pid_, SIGKILL);
while (waitpid(pid_, nullptr, 0) < 0 && errno == EINTR) {
}
pid_ = 0;
}
}
if (shared_)
munmap(shared_, sharedSize_);
shared_ = nullptr;
sharedSize_ = 0;
closeFd(sharedFd_);
logBuffer_.clear();
}
void readFrames()
{
uint8_t frameSlots[128];
uint8_t latest = OWB_SHARED_FRAME_SLOTS;
for (;;) {
ssize_t count = read(frameFd_, frameSlots, sizeof(frameSlots));
if (count < 0 && errno == EINTR)
continue;
if (count <= 0)
break;
for (ssize_t i = 0; i < count; ++i)
if (frameSlots[i] < OWB_SHARED_FRAME_SLOTS)
latest = frameSlots[i];
}
if (!shared_ || latest >= OWB_SHARED_FRAME_SLOTS)
return;
for (unsigned attempt = 0; attempt < 3; ++attempt) {
uint64_t before = __atomic_load_n(&shared_->sequence[latest], __ATOMIC_ACQUIRE);
if (before & 1u)
continue;
QImage view(owb_shared_slot(shared_, latest), renderWidth_, renderHeight_, renderWidth_ * 4,
QImage::Format_ARGB32);
QImage copy = view.copy();
uint64_t after = __atomic_load_n(&shared_->sequence[latest], __ATOMIC_ACQUIRE);
if (before == after && !(after & 1u)) {
frame_ = std::move(copy);
status_.clear();
update();
break;
}
}
}
void readLogs()
{
char bytes[1024];
for (;;) {
ssize_t count = read(logFd_, bytes, sizeof(bytes));
if (count < 0 && errno == EINTR)
continue;
if (count <= 0)
break;
logBuffer_.append(bytes, static_cast<qsizetype>(count));
}
for (;;) {
qsizetype newline = logBuffer_.indexOf('\n');
if (newline < 0)
break;
QByteArray line = logBuffer_.left(newline).trimmed();
logBuffer_.remove(0, newline + 1);
if (!line.isEmpty())
blog(LOG_INFO, "[obs-webkit-browser dock] %s", line.constData());
}
}
QString url_;
QString status_;
QImage frame_;
QTimer resizeTimer_;
pid_t pid_ = 0;
int controlFd_ = -1;
int frameFd_ = -1;
int logFd_ = -1;
int sharedFd_ = -1;
owb_shared_frames *shared_ = nullptr;
size_t sharedSize_ = 0;
int renderWidth_ = 2;
int renderHeight_ = 2;
QSocketNotifier *frameNotifier_ = nullptr;
QSocketNotifier *logNotifier_ = nullptr;
QByteArray logBuffer_;
};
class DockManager final {
public:
DockManager()
{
obs_frontend_add_save_callback(saveCallback, this);
obs_frontend_add_preload_callback(preloadCallback, this);
obs_frontend_add_event_callback(eventCallback, this);
setupFrontend();
}
~DockManager()
{
/* OBS clears frontend callbacks and owns the dock wrappers before
* unloading modules during shutdown. Calling frontend removal APIs
* from obs_module_unload is therefore too late. */
refreshButtonTimer_.stop();
delete refreshButton_;
panels_.clear();
if (menuAction_)
delete menuAction_;
}
void setupFrontend()
{
if (!QApplication::instance())
return;
auto *main = static_cast<QMainWindow *>(obs_frontend_get_main_window());
if (!main)
return;
if (!menuAction_) {
menuAction_ = new QAction(text("CustomBrowserDocks"), main);
QObject::connect(menuAction_, &QAction::triggered, main, [this] { showEditor(); });
if (QMenu *docksMenu = main->findChild<QMenu *>("menuDocks"))
docksMenu->addAction(menuAction_);
else
main->menuBar()->addAction(menuAction_);
}
setupRefreshButton(main);
reconcile();
}
private:
static QString dockId(const QString &id) { return QString::fromUtf8(kDockPrefix) + id; }
static constexpr const char *kSourceId = "webkit_browser_source";
obs_source_t *currentContextSource() const
{
if (!contextSourceLabel_)
return nullptr;
QByteArray name = contextSourceLabel_->text().toUtf8();
return obs_get_source_by_name(name.constData());
}
void updateRefreshButton()
{
if (!refreshButton_)
return;
bool visible = false;
obs_source_t *source = currentContextSource();
if (source) {
const char *id = obs_source_get_unversioned_id(source);
visible = id && strcmp(id, kSourceId) == 0;
obs_source_release(source);
}
refreshButton_->setVisible(visible);
if (sourceInteractButton_)
refreshButton_->setText(sourceInteractButton_->text().isEmpty() ? QString() : text("Refresh"));
}
void setupRefreshButton(QMainWindow *main)
{
if (refreshButton_)
return;
sourceInteractButton_ = main->findChild<QPushButton *>("sourceInteractButton");
contextSourceLabel_ = main->findChild<QLabel *>("contextSourceLabel");
if (!sourceInteractButton_ || !contextSourceLabel_)
return;
auto *layout = qobject_cast<QBoxLayout *>(sourceInteractButton_->parentWidget()->layout());
if (!layout)
return;
refreshButton_ = new QPushButton(text("Refresh"), sourceInteractButton_->parentWidget());
refreshButton_->setObjectName("webkitBrowserRefreshButton");
refreshButton_->setMinimumHeight(22);
refreshButton_->setToolTip(text("Refresh"));
refreshButton_->setIcon(QIcon(":/res/images/refresh.svg"));
refreshButton_->setProperty("class", "icon-refresh");
layout->insertWidget(layout->indexOf(sourceInteractButton_) + 1, refreshButton_);
QObject::connect(refreshButton_, &QPushButton::clicked, main, [this] {
obs_source_t *source = currentContextSource();
if (!source)
return;
const char *id = obs_source_get_unversioned_id(source);
if (id && strcmp(id, kSourceId) == 0) {
obs_properties_t *properties = obs_source_properties(source);
obs_property_t *reload = properties ? obs_properties_get(properties, "reload") : nullptr;
if (reload)
obs_property_button_clicked(reload, source);
if (properties)
obs_properties_destroy(properties);
}
obs_source_release(source);
});
refreshButtonTimer_.setInterval(100);
QObject::connect(&refreshButtonTimer_, &QTimer::timeout, main, [this] { updateRefreshButton(); });
refreshButtonTimer_.start();
updateRefreshButton();
}
static void eventCallback(obs_frontend_event event, void *opaque)
{
auto *manager = static_cast<DockManager *>(opaque);
if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING)
manager->setupFrontend();
else if (event == OBS_FRONTEND_EVENT_EXIT) {
manager->panels_.clear();
manager->menuAction_ = nullptr;
}
}
static void saveCallback(obs_data_t *saveData, bool saving, void *opaque)
{
static_cast<DockManager *>(opaque)->saveLoad(saveData, saving);
}
static void preloadCallback(obs_data_t *saveData, bool saving, void *opaque)
{
if (!saving)
static_cast<DockManager *>(opaque)->saveLoad(saveData, false);
}
void saveLoad(obs_data_t *saveData, bool saving)
{
if (saving) {
obs_data_array_t *array = obs_data_array_create();
for (const DockDefinition &definition : definitions_) {
obs_data_t *item = obs_data_create();
obs_data_set_string(item, "id", definition.id.toUtf8().constData());
obs_data_set_string(item, "title", definition.title.toUtf8().constData());
obs_data_set_string(item, "url", definition.url.toUtf8().constData());
obs_data_array_push_back(array, item);
obs_data_release(item);
}
obs_data_set_array(saveData, kSaveKey, array);
obs_data_array_release(array);
return;
}
definitions_.clear();
obs_data_array_t *array = obs_data_get_array(saveData, kSaveKey);
if (array) {
size_t count = obs_data_array_count(array);
for (size_t index = 0; index < count; ++index) {
obs_data_t *item = obs_data_array_item(array, index);
DockDefinition definition{
QString::fromUtf8(obs_data_get_string(item, "id")),
QString::fromUtf8(obs_data_get_string(item, "title")),
QString::fromUtf8(obs_data_get_string(item, "url")),
};
if (definition.id.isEmpty())
definition.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
if (!definition.title.trimmed().isEmpty() && !definition.url.trimmed().isEmpty())
definitions_.push_back(std::move(definition));
obs_data_release(item);
}
obs_data_array_release(array);
}
reconcile();
}
void reconcile()
{
if (!menuAction_)
return;
QHash<QString, DockDefinition> wanted;
for (const DockDefinition &definition : definitions_)
wanted.insert(definition.id, definition);
for (auto it = panels_.begin(); it != panels_.end();) {
if (!wanted.contains(it.key())) {
QByteArray id = dockId(it.key()).toUtf8();
obs_frontend_remove_dock(id.constData());
it = panels_.erase(it);
} else {
++it;
}
}
for (const DockDefinition &definition : definitions_) {
if (panels_.contains(definition.id) && panels_[definition.id]) {
panels_[definition.id]->setUrl(definition.url);
panels_[definition.id]->setDockTitle(definition.title);
continue;
}
auto *panel = new WebPanel(definition.url);
QByteArray id = dockId(definition.id).toUtf8();
QByteArray title = definition.title.toUtf8();
if (obs_frontend_add_dock_by_id(id.constData(), title.constData(), panel))
panels_.insert(definition.id, panel);
else
delete panel;
}
}
void showEditor()
{
auto *main = static_cast<QWidget *>(obs_frontend_get_main_window());
QDialog dialog(main);
dialog.setWindowTitle(text("CustomBrowserDocks"));
dialog.resize(720, 360);
auto *layout = new QVBoxLayout(&dialog);
layout->addWidget(new QLabel(text("CustomBrowserDocksDescription"), &dialog));
auto *table = new QTableWidget(0, 2, &dialog);
table->setHorizontalHeaderLabels({text("PanelName"), text("URL")});
table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->setSelectionMode(QAbstractItemView::ExtendedSelection);
layout->addWidget(table);
auto appendRow = [table](const DockDefinition &definition) {
int row = table->rowCount();
table->insertRow(row);
auto *title = new QTableWidgetItem(definition.title);
title->setData(Qt::UserRole, definition.id);
table->setItem(row, 0, title);
table->setItem(row, 1, new QTableWidgetItem(definition.url));
};
for (const DockDefinition &definition : definitions_)
appendRow(definition);
auto *rowButtons = new QHBoxLayout;
auto *add = new QPushButton(text("AddPanel"), &dialog);
auto *remove = new QPushButton(text("RemovePanel"), &dialog);
rowButtons->addWidget(add);
rowButtons->addWidget(remove);
rowButtons->addStretch();
layout->addLayout(rowButtons);
QObject::connect(add, &QPushButton::clicked, table, [table, appendRow] {
appendRow({QUuid::createUuid().toString(QUuid::WithoutBraces), QString(), "https://"});
int row = table->rowCount() - 1;
table->setCurrentCell(row, 0);
table->editItem(table->item(row, 0));
});
QObject::connect(remove, &QPushButton::clicked, table, [table] {
QSet<int> rows;
for (QTableWidgetItem *item : table->selectedItems())
rows.insert(item->row());
QList<int> sorted = rows.values();
std::sort(sorted.begin(), sorted.end(), std::greater<int>());
for (int row : sorted)
table->removeRow(row);
});
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Apply | QDialogButtonBox::Close,
Qt::Horizontal, &dialog);
layout->addWidget(buttons);
QObject::connect(buttons->button(QDialogButtonBox::Close), &QPushButton::clicked, &dialog,
&QDialog::reject);
QObject::connect(buttons->button(QDialogButtonBox::Apply), &QPushButton::clicked, &dialog,
[this, table, &dialog] {
QVector<DockDefinition> updated;
QSet<QString> titles;
for (int row = 0; row < table->rowCount(); ++row) {
QString title = table->item(row, 0) ? table->item(row, 0)->text().trimmed() : QString();
QString url = table->item(row, 1) ? table->item(row, 1)->text().trimmed() : QString();
if (title.isEmpty() || url.isEmpty()) {
QMessageBox::warning(&dialog, text("CustomBrowserDocks"), text("PanelFieldsRequired"));
return;
}
if (titles.contains(title)) {
QMessageBox::warning(&dialog, text("CustomBrowserDocks"), text("PanelNamesUnique"));
return;
}
titles.insert(title);
QString id = table->item(row, 0)->data(Qt::UserRole).toString();
if (id.isEmpty())
id = QUuid::createUuid().toString(QUuid::WithoutBraces);
updated.push_back({id, title, url});
}
definitions_ = std::move(updated);
reconcile();
obs_frontend_save();
});
dialog.exec();
}
QVector<DockDefinition> definitions_;
QHash<QString, QPointer<WebPanel>> panels_;
QPointer<QAction> menuAction_;
QPointer<QPushButton> sourceInteractButton_;
QPointer<QLabel> contextSourceLabel_;
QPointer<QPushButton> refreshButton_;
QTimer refreshButtonTimer_;
};
DockManager *manager = nullptr;
} // namespace
extern "C" void owb_docks_initialize(void)
{
if (!manager && QApplication::instance())
manager = new DockManager;
}
extern "C" void owb_docks_shutdown(void)
{
delete manager;
manager = nullptr;
}

View File

@@ -29,6 +29,14 @@ MODULE_EXPORT const char *obs_module_description(void)
return "WebKitGTK browser source for OBS (no CEF)"; return "WebKitGTK browser source for OBS (no CEF)";
} }
void owb_docks_initialize(void);
void owb_docks_shutdown(void);
const char *owb_module_text(const char *key)
{
return obs_module_text(key);
}
extern char **environ; extern char **environ;
struct browser_source; struct browser_source;
@@ -295,7 +303,7 @@ static bool start_renderer_locked(struct browser_source *browser)
shared->width = browser->width; shared->width = browser->width;
shared->height = browser->height; shared->height = browser->height;
shared->stride = browser->width * 4; shared->stride = browser->width * 4;
shared->slots = OWB_SHARED_FRAME_SLOTS; shared->slot_count = OWB_SHARED_FRAME_SLOTS;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) != 0 || pipe(frames) != 0 || pipe(logs) != 0) { 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)); blog(LOG_ERROR, "[obs-webkit-browser] IPC setup failed: %s", strerror(errno));
goto fail; goto fail;
@@ -581,6 +589,7 @@ static const char *browser_name(void *unused)
static struct obs_source_info browser_info = { static struct obs_source_info browser_info = {
.id = "webkit_browser_source", .id = "webkit_browser_source",
.type = OBS_SOURCE_TYPE_INPUT, .type = OBS_SOURCE_TYPE_INPUT,
.icon_type = OBS_ICON_TYPE_BROWSER,
.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_INTERACTION, .output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_INTERACTION,
.get_name = browser_name, .get_name = browser_name,
.create = browser_create, .create = browser_create,
@@ -602,6 +611,12 @@ static struct obs_source_info browser_info = {
bool obs_module_load(void) bool obs_module_load(void)
{ {
obs_register_source(&browser_info); obs_register_source(&browser_info);
owb_docks_initialize();
blog(LOG_INFO, "[obs-webkit-browser] Loaded WebKitGTK browser source"); blog(LOG_INFO, "[obs-webkit-browser] Loaded WebKitGTK browser source");
return true; return true;
} }
void obs_module_unload(void)
{
owb_docks_shutdown();
}

View File

@@ -29,9 +29,9 @@ struct owb_shared_frames {
uint32_t width; uint32_t width;
uint32_t height; uint32_t height;
uint32_t stride; uint32_t stride;
uint32_t slots; uint32_t slot_count;
uint64_t sequence[OWB_SHARED_FRAME_SLOTS]; uint64_t sequence[OWB_SHARED_FRAME_SLOTS];
uint8_t pixels[]; uint8_t pixels[1];
}; };
static inline size_t owb_frame_size(uint32_t width, uint32_t height) static inline size_t owb_frame_size(uint32_t width, uint32_t height)
@@ -41,7 +41,7 @@ static inline size_t owb_frame_size(uint32_t width, uint32_t height)
static inline size_t owb_shared_size(uint32_t width, uint32_t height) static inline size_t owb_shared_size(uint32_t width, uint32_t height)
{ {
return sizeof(struct owb_shared_frames) + return offsetof(struct owb_shared_frames, pixels) +
owb_frame_size(width, height) * OWB_SHARED_FRAME_SLOTS; owb_frame_size(width, height) * OWB_SHARED_FRAME_SLOTS;
} }

View File

@@ -14,15 +14,19 @@
#include <limits.h> #include <limits.h>
#include <pthread.h> #include <pthread.h>
#include <signal.h> #include <signal.h>
#include <stdarg.h>
#include <stdatomic.h> #include <stdatomic.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/mman.h> #include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/un.h>
#include <sys/ipc.h> #include <sys/ipc.h>
#include <sys/shm.h> #include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
#include "protocol.h" #include "protocol.h"
@@ -40,6 +44,13 @@ struct renderer {
struct config config; struct config config;
GtkWidget *window; GtkWidget *window;
WebKitWebView *web_view; WebKitWebView *web_view;
int control_fd;
int frame_fd;
int log_fd;
int shared_fd;
int client_fd;
guint control_source;
guint client_source;
struct owb_shared_frames *shared; struct owb_shared_frames *shared;
size_t shared_size; size_t shared_size;
uint64_t next_sequence; uint64_t next_sequence;
@@ -59,6 +70,22 @@ struct renderer {
bool capture_attached; bool capture_attached;
}; };
static GList *renderers;
static WebKitWebContext *shared_context;
static guint idle_source;
static void destroy_renderer(struct renderer *renderer);
static void renderer_log(struct renderer *renderer, const char *format, ...)
{
if (!renderer || renderer->log_fd < 0)
return;
va_list arguments;
va_start(arguments, format);
vdprintf(renderer->log_fd, format, arguments);
va_end(arguments);
}
static bool read_exact(int fd, void *data, size_t size) static bool read_exact(int fd, void *data, size_t size)
{ {
uint8_t *cursor = data; uint8_t *cursor = data;
@@ -74,10 +101,10 @@ static bool read_exact(int fd, void *data, size_t size)
return true; return true;
} }
static bool read_config(struct config *config) static bool read_config(int fd, struct config *config)
{ {
struct owb_config_header header; struct owb_config_header header;
if (!read_exact(STDIN_FILENO, &header, sizeof(header)) || header.magic != OWB_CONFIG_MAGIC || if (!read_exact(fd, &header, sizeof(header)) || header.magic != OWB_CONFIG_MAGIC ||
header.version != OWB_PROTOCOL_VERSION || header.width < 2 || header.height < 2 || header.version != OWB_PROTOCOL_VERSION || header.width < 2 || header.height < 2 ||
header.width > 8192 || header.height > 8192 || header.fps < 1 || header.fps > 60 || header.width > 8192 || header.height > 8192 || header.fps < 1 || header.fps > 60 ||
header.url_length > OWB_MAX_STRING || header.css_length > OWB_MAX_STRING) header.url_length > OWB_MAX_STRING || header.css_length > OWB_MAX_STRING)
@@ -93,8 +120,8 @@ static bool read_config(struct config *config)
config->css = calloc((size_t)header.css_length + 1, 1); config->css = calloc((size_t)header.css_length + 1, 1);
if (!config->url || !config->css) if (!config->url || !config->css)
return false; return false;
return read_exact(STDIN_FILENO, config->url, header.url_length) && return read_exact(fd, config->url, header.url_length) &&
read_exact(STDIN_FILENO, config->css, header.css_length); read_exact(fd, config->css, header.css_length);
} }
static char *normalize_uri(const char *value) static char *normalize_uri(const char *value)
@@ -162,10 +189,10 @@ static bool map_shared_frames(struct renderer *renderer)
{ {
struct stat info; struct stat info;
renderer->shared_size = owb_shared_size(renderer->config.width, renderer->config.height); 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) if (fstat(renderer->shared_fd, &info) != 0 || (size_t)info.st_size < renderer->shared_size)
return false; return false;
renderer->shared = mmap(NULL, renderer->shared_size, PROT_READ | PROT_WRITE, MAP_SHARED, renderer->shared = mmap(NULL, renderer->shared_size, PROT_READ | PROT_WRITE, MAP_SHARED,
OWB_SHARED_FRAME_FD, 0); renderer->shared_fd, 0);
if (renderer->shared == MAP_FAILED) if (renderer->shared == MAP_FAILED)
return false; return false;
return renderer->shared->magic == OWB_SHARED_MAGIC && return renderer->shared->magic == OWB_SHARED_MAGIC &&
@@ -173,7 +200,7 @@ static bool map_shared_frames(struct renderer *renderer)
renderer->shared->width == renderer->config.width && renderer->shared->width == renderer->config.width &&
renderer->shared->height == renderer->config.height && renderer->shared->height == renderer->config.height &&
renderer->shared->stride == renderer->config.width * 4 && renderer->shared->stride == renderer->config.width * 4 &&
renderer->shared->slots == OWB_SHARED_FRAME_SLOTS; renderer->shared->slot_count == OWB_SHARED_FRAME_SLOTS;
} }
static bool initialize_x11_capture(struct renderer *renderer) static bool initialize_x11_capture(struct renderer *renderer)
@@ -299,7 +326,7 @@ static void publish_frame(struct renderer *renderer, const uint8_t *pixels, int
} }
__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;
ssize_t ignored = write(STDOUT_FILENO, &notification, sizeof(notification)); ssize_t ignored = write(renderer->frame_fd, &notification, sizeof(notification));
(void)ignored; (void)ignored;
} }
@@ -352,7 +379,7 @@ static void *capture_loop(void *opaque)
None, None); None, None);
bool published = publish_x11_frame(renderer); bool published = publish_x11_frame(renderer);
if (published && !transport_reported && process_uses_drm_render_node()) { if (published && !transport_reported && process_uses_drm_render_node()) {
fprintf(stderr, "DRM render node: active\n"); renderer_log(renderer, "DRM render node: active\n");
transport_reported = true; transport_reported = true;
} }
first_frame = false; first_frame = false;
@@ -386,6 +413,16 @@ static GdkModifierType modifiers_from_obs(uint32_t modifiers)
return state; return state;
} }
static void set_event_device(GdkEvent *event, GdkWindow *window, bool keyboard)
{
GdkDisplay *display = gdk_window_get_display(window);
GdkSeat *seat = display ? gdk_display_get_default_seat(display) : NULL;
GdkDevice *device = seat ? (keyboard ? gdk_seat_get_keyboard(seat) : gdk_seat_get_pointer(seat))
: NULL;
if (device)
gdk_event_set_device(event, device);
}
static void dispatch_command(struct renderer *renderer, const struct owb_command *command) static void dispatch_command(struct renderer *renderer, const struct owb_command *command)
{ {
if (command->magic != OWB_COMMAND_MAGIC) if (command->magic != OWB_COMMAND_MAGIC)
@@ -405,6 +442,7 @@ static void dispatch_command(struct renderer *renderer, const struct owb_command
if (command->type == OWB_MOUSE_MOVE) { if (command->type == OWB_MOUSE_MOVE) {
if (command->value1) { if (command->value1) {
GdkEvent *event = gdk_event_new(GDK_LEAVE_NOTIFY); GdkEvent *event = gdk_event_new(GDK_LEAVE_NOTIFY);
set_event_device(event, window, false);
event->crossing.window = g_object_ref(window); event->crossing.window = g_object_ref(window);
event->crossing.send_event = TRUE; event->crossing.send_event = TRUE;
event->crossing.time = GDK_CURRENT_TIME; event->crossing.time = GDK_CURRENT_TIME;
@@ -418,6 +456,7 @@ static void dispatch_command(struct renderer *renderer, const struct owb_command
return; return;
} }
GdkEvent *event = gdk_event_new(GDK_MOTION_NOTIFY); GdkEvent *event = gdk_event_new(GDK_MOTION_NOTIFY);
set_event_device(event, window, false);
event->motion.window = g_object_ref(window); event->motion.window = g_object_ref(window);
event->motion.send_event = TRUE; event->motion.send_event = TRUE;
event->motion.time = GDK_CURRENT_TIME; event->motion.time = GDK_CURRENT_TIME;
@@ -434,6 +473,7 @@ static void dispatch_command(struct renderer *renderer, const struct owb_command
: command->text_length >= 3 ? GDK_3BUTTON_PRESS : command->text_length >= 3 ? GDK_3BUTTON_PRESS
: GDK_BUTTON_PRESS; : GDK_BUTTON_PRESS;
GdkEvent *event = gdk_event_new(type); GdkEvent *event = gdk_event_new(type);
set_event_device(event, window, false);
event->button.window = g_object_ref(window); event->button.window = g_object_ref(window);
event->button.send_event = TRUE; event->button.send_event = TRUE;
event->button.time = GDK_CURRENT_TIME; event->button.time = GDK_CURRENT_TIME;
@@ -447,6 +487,7 @@ static void dispatch_command(struct renderer *renderer, const struct owb_command
} }
if (command->type == OWB_MOUSE_WHEEL) { if (command->type == OWB_MOUSE_WHEEL) {
GdkEvent *event = gdk_event_new(GDK_SCROLL); GdkEvent *event = gdk_event_new(GDK_SCROLL);
set_event_device(event, window, false);
event->scroll.window = g_object_ref(window); event->scroll.window = g_object_ref(window);
event->scroll.send_event = TRUE; event->scroll.send_event = TRUE;
event->scroll.time = GDK_CURRENT_TIME; event->scroll.time = GDK_CURRENT_TIME;
@@ -462,6 +503,7 @@ static void dispatch_command(struct renderer *renderer, const struct owb_command
} }
if (command->type == OWB_KEY) { if (command->type == OWB_KEY) {
GdkEvent *event = gdk_event_new(command->value1 ? GDK_KEY_RELEASE : GDK_KEY_PRESS); GdkEvent *event = gdk_event_new(command->value1 ? GDK_KEY_RELEASE : GDK_KEY_PRESS);
set_event_device(event, window, true);
event->key.window = g_object_ref(window); event->key.window = g_object_ref(window);
event->key.send_event = TRUE; event->key.send_event = TRUE;
event->key.time = GDK_CURRENT_TIME; event->key.time = GDK_CURRENT_TIME;
@@ -483,7 +525,8 @@ static gboolean control_ready(gint fd, GIOCondition condition, gpointer opaque)
{ {
struct renderer *renderer = opaque; struct renderer *renderer = opaque;
if (condition & (G_IO_HUP | G_IO_ERR)) { if (condition & (G_IO_HUP | G_IO_ERR)) {
gtk_main_quit(); renderer->control_source = 0;
destroy_renderer(renderer);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
for (;;) { for (;;) {
@@ -494,7 +537,8 @@ static gboolean control_ready(gint fd, GIOCondition condition, gpointer opaque)
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
return G_SOURCE_CONTINUE; return G_SOURCE_CONTINUE;
if (count <= 0) { if (count <= 0) {
gtk_main_quit(); renderer->control_source = 0;
destroy_renderer(renderer);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
renderer->command_used += (size_t)count; renderer->command_used += (size_t)count;
@@ -515,22 +559,14 @@ static gboolean clear_window(GtkWidget *widget, cairo_t *context, gpointer unuse
return FALSE; 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, static void script_message_received(WebKitUserContentManager *manager,
WebKitJavascriptResult *message, gpointer unused) WebKitJavascriptResult *message, gpointer opaque)
{ {
(void)manager; (void)manager;
(void)unused; struct renderer *renderer = opaque;
JSCValue *value = webkit_javascript_result_get_js_value(message); JSCValue *value = webkit_javascript_result_get_js_value(message);
char *text = jsc_value_to_string(value); char *text = jsc_value_to_string(value);
fprintf(stderr, "Page: %s\n", text ? text : "(empty message)"); renderer_log(renderer, "Page: %s\n", text ? text : "(empty message)");
g_free(text); g_free(text);
} }
@@ -539,34 +575,102 @@ static gboolean load_failed(WebKitWebView *view, WebKitLoadEvent event, const ch
{ {
(void)view; (void)view;
(void)event; (void)event;
(void)unused; struct renderer *renderer = unused;
fprintf(stderr, "Page load failed for %s: %s\n", uri, error->message); renderer_log(renderer, "Page load failed for %s: %s\n", uri, error->message);
return FALSE; return FALSE;
} }
int main(void) static gboolean exit_when_idle(gpointer unused)
{ {
struct renderer renderer = {0}; (void)unused;
atomic_init(&renderer.stopping, false); idle_source = 0;
if (!read_config(&renderer.config)) { if (!renderers)
fprintf(stderr, "Invalid renderer configuration\n"); gtk_main_quit();
return 2; return G_SOURCE_REMOVE;
} }
prepare_webkit();
if (!map_shared_frames(&renderer)) { static void destroy_renderer(struct renderer *renderer)
fprintf(stderr, "Cannot map shared frame transport\n"); {
return 4; if (!renderer)
return;
renderers = g_list_remove(renderers, renderer);
if (renderer->control_source)
g_source_remove(renderer->control_source);
if (renderer->client_source)
g_source_remove(renderer->client_source);
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->window)
gtk_widget_destroy(renderer->window);
if (renderer->shared && renderer->shared != MAP_FAILED)
munmap(renderer->shared, renderer->shared_size);
if (renderer->control_fd >= 0)
close(renderer->control_fd);
if (renderer->frame_fd >= 0)
close(renderer->frame_fd);
if (renderer->log_fd >= 0)
close(renderer->log_fd);
if (renderer->shared_fd >= 0)
close(renderer->shared_fd);
if (renderer->client_fd >= 0)
close(renderer->client_fd);
free(renderer->config.url);
free(renderer->config.css);
free(renderer);
if (!renderers && !idle_source)
idle_source = g_timeout_add_seconds(2, exit_when_idle, NULL);
} }
if (!gtk_init_check(NULL, NULL)) {
fprintf(stderr, "GTK could not connect to the OBS display\n"); static gboolean client_ready(gint fd, GIOCondition condition, gpointer opaque)
return 5; {
struct renderer *renderer = opaque;
if (condition & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) {
renderer->client_source = 0;
destroy_renderer(renderer);
return G_SOURCE_REMOVE;
}
char ignored[16];
ssize_t count = recv(fd, ignored, sizeof(ignored), MSG_DONTWAIT);
if (count == 0) {
renderer->client_source = 0;
destroy_renderer(renderer);
return G_SOURCE_REMOVE;
}
return G_SOURCE_CONTINUE;
}
static struct renderer *create_renderer(int client_fd, const int descriptors[4])
{
struct renderer *renderer = calloc(1, sizeof(*renderer));
if (!renderer) {
for (size_t index = 0; index < 4; index++)
close(descriptors[index]);
close(client_fd);
return NULL;
}
renderer->control_fd = descriptors[0];
renderer->frame_fd = descriptors[1];
renderer->log_fd = descriptors[2];
renderer->shared_fd = descriptors[3];
renderer->client_fd = client_fd;
renderer->capture_shm.shmid = -1;
atomic_init(&renderer->stopping, false);
if (!read_config(renderer->control_fd, &renderer->config)) {
renderer_log(renderer, "Invalid renderer configuration\n");
goto fail;
}
if (!map_shared_frames(renderer)) {
renderer_log(renderer, "Cannot map shared frame transport\n");
goto fail;
} }
WebKitUserContentManager *manager = webkit_user_content_manager_new(); WebKitUserContentManager *manager = webkit_user_content_manager_new();
g_signal_connect(manager, "script-message-received::owbLog", g_signal_connect(manager, "script-message-received::owbLog",
G_CALLBACK(script_message_received), NULL); G_CALLBACK(script_message_received), renderer);
if (!webkit_user_content_manager_register_script_message_handler(manager, "owbLog")) if (!webkit_user_content_manager_register_script_message_handler(manager, "owbLog"))
fprintf(stderr, "Cannot register page diagnostic message handler\n"); renderer_log(renderer, "Cannot register page diagnostic message handler\n");
static const char error_reporter[] = static const char error_reporter[] =
"(()=>{const p=(k,v)=>{try{window.webkit.messageHandlers.owbLog.postMessage(k+': '+v)}catch{}};" "(()=>{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('error',e=>p('error',e.message+' @ '+e.filename+':'+e.lineno));"
@@ -577,7 +681,7 @@ 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) { if (renderer->config.transparent) {
static const char transparent_page_css[] = static const char transparent_page_css[] =
"html,body{background:transparent!important}"; "html,body{background:transparent!important}";
WebKitUserStyleSheet *transparent_sheet = webkit_user_style_sheet_new( WebKitUserStyleSheet *transparent_sheet = webkit_user_style_sheet_new(
@@ -586,96 +690,303 @@ int main(void)
webkit_user_content_manager_add_style_sheet(manager, transparent_sheet); webkit_user_content_manager_add_style_sheet(manager, transparent_sheet);
webkit_user_style_sheet_unref(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);
webkit_user_content_manager_add_style_sheet(manager, sheet); webkit_user_content_manager_add_style_sheet(manager, sheet);
webkit_user_style_sheet_unref(sheet); webkit_user_style_sheet_unref(sheet);
} }
renderer.web_view = WEBKIT_WEB_VIEW(webkit_web_view_new_with_user_content_manager(manager)); renderer->web_view = WEBKIT_WEB_VIEW(g_object_new(
g_signal_connect(renderer.web_view, "load-failed", G_CALLBACK(load_failed), NULL); WEBKIT_TYPE_WEB_VIEW, "web-context", shared_context,
"user-content-manager", manager, NULL));
g_signal_connect(renderer->web_view, "load-failed", G_CALLBACK(load_failed), renderer);
g_object_unref(manager); g_object_unref(manager);
WebKitSettings *settings = webkit_web_view_get_settings(renderer.web_view); WebKitSettings *settings = webkit_web_view_get_settings(renderer->web_view);
webkit_settings_set_enable_html5_local_storage(settings, TRUE);
webkit_settings_set_enable_webgl(settings, TRUE); webkit_settings_set_enable_webgl(settings, TRUE);
webkit_settings_set_enable_page_cache(settings, TRUE); webkit_settings_set_enable_page_cache(settings, TRUE);
webkit_settings_set_enable_smooth_scrolling(settings, TRUE); webkit_settings_set_enable_smooth_scrolling(settings, TRUE);
webkit_settings_set_media_playback_requires_user_gesture(settings, FALSE); webkit_settings_set_media_playback_requires_user_gesture(settings, FALSE);
webkit_settings_set_hardware_acceleration_policy(settings, webkit_settings_set_hardware_acceleration_policy(settings,
WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS); WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
webkit_web_view_set_is_muted(renderer.web_view, TRUE); 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}; 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); webkit_web_view_set_background_color(renderer->web_view, &background);
renderer.window = gtk_window_new(GTK_WINDOW_POPUP); renderer->window = gtk_window_new(GTK_WINDOW_POPUP);
GdkScreen *screen = gdk_screen_get_default(); GdkScreen *screen = gdk_screen_get_default();
GdkVisual *visual = renderer.config.transparent ? gdk_screen_get_rgba_visual(screen) GdkVisual *visual = renderer->config.transparent ? gdk_screen_get_rgba_visual(screen)
: gdk_screen_get_system_visual(screen); : gdk_screen_get_system_visual(screen);
if (visual) if (visual)
gtk_widget_set_visual(renderer.window, visual); gtk_widget_set_visual(renderer->window, visual);
gtk_widget_set_app_paintable(renderer.window, renderer.config.transparent); gtk_widget_set_app_paintable(renderer->window, renderer->config.transparent);
if (renderer.config.transparent) if (renderer->config.transparent)
g_signal_connect(renderer.window, "draw", G_CALLBACK(clear_window), NULL); g_signal_connect(renderer->window, "draw", G_CALLBACK(clear_window), NULL);
gtk_window_set_decorated(GTK_WINDOW(renderer.window), FALSE); gtk_window_set_decorated(GTK_WINDOW(renderer->window), FALSE);
gtk_window_set_resizable(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_taskbar_hint(GTK_WINDOW(renderer->window), TRUE);
gtk_window_set_skip_pager_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_accept_focus(GTK_WINDOW(renderer->window), FALSE);
gtk_window_set_focus_on_map(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_move(GTK_WINDOW(renderer->window), 0, 0);
gtk_window_set_default_size(GTK_WINDOW(renderer.window), (int)renderer.config.width, gtk_window_set_default_size(GTK_WINDOW(renderer->window), (int)renderer->config.width,
(int)renderer.config.height); (int)renderer->config.height);
gtk_widget_set_size_request(GTK_WIDGET(renderer.web_view), (int)renderer.config.width, gtk_widget_set_size_request(GTK_WIDGET(renderer->web_view), (int)renderer->config.width,
(int)renderer.config.height); (int)renderer->config.height);
gtk_container_add(GTK_CONTAINER(renderer.window), GTK_WIDGET(renderer.web_view)); gtk_container_add(GTK_CONTAINER(renderer->window), GTK_WIDGET(renderer->web_view));
gtk_widget_realize(renderer.window); gtk_widget_realize(renderer->window);
GdkWindow *renderer_window = gtk_widget_get_window(renderer.window); GdkWindow *renderer_window = gtk_widget_get_window(renderer->window);
if (!renderer_window || gdk_window_get_window_type(renderer_window) == GDK_WINDOW_OFFSCREEN) { if (!renderer_window || gdk_window_get_window_type(renderer_window) == GDK_WINDOW_OFFSCREEN) {
fprintf(stderr, "GTK did not create the native window required for GPU rendering\n"); renderer_log(renderer, "GTK did not create the native window required for GPU rendering\n");
return 6; goto fail;
} }
cairo_region_t *empty_input = cairo_region_create(); cairo_region_t *empty_input = cairo_region_create();
gdk_window_input_shape_combine_region(renderer_window, empty_input, 0, 0); gdk_window_input_shape_combine_region(renderer_window, empty_input, 0, 0);
cairo_region_destroy(empty_input); cairo_region_destroy(empty_input);
gdk_window_set_opacity(renderer_window, 0.0); gdk_window_set_opacity(renderer_window, 0.0);
gtk_widget_show_all(renderer.window); gtk_widget_show_all(renderer->window);
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/XDamage/XShm GPU capture\n"); renderer_log(renderer, "Cannot initialize XComposite/XDamage/XShm GPU capture\n");
return 7; goto fail;
} }
int output_flags = fcntl(STDOUT_FILENO, F_GETFL, 0); int output_flags = fcntl(renderer->frame_fd, F_GETFL, 0);
if (output_flags >= 0) if (output_flags >= 0)
fcntl(STDOUT_FILENO, F_SETFL, output_flags | O_NONBLOCK); fcntl(renderer->frame_fd, F_SETFL, output_flags | O_NONBLOCK);
int flags = fcntl(STDIN_FILENO, F_GETFL, 0); int flags = fcntl(renderer->control_fd, F_GETFL, 0);
if (flags >= 0) if (flags >= 0)
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); fcntl(renderer->control_fd, F_SETFL, flags | O_NONBLOCK);
g_unix_fd_add(STDIN_FILENO, G_IO_IN | G_IO_HUP | G_IO_ERR, control_ready, &renderer); renderer->control_source = g_unix_fd_add(renderer->control_fd,
char *uri = normalize_uri(renderer.config.url); G_IO_IN | G_IO_HUP | G_IO_ERR, control_ready, renderer);
webkit_web_view_load_uri(renderer.web_view, uri); renderer->client_source = g_unix_fd_add(renderer->client_fd,
G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, client_ready, renderer);
char *uri = normalize_uri(renderer->config.url);
webkit_web_view_load_uri(renderer->web_view, uri);
g_free(uri); g_free(uri);
if (pthread_create(&renderer.capture_thread, NULL, capture_loop, &renderer) != 0) { if (pthread_create(&renderer->capture_thread, NULL, capture_loop, renderer) != 0) {
fprintf(stderr, "Cannot start GPU capture thread\n"); renderer_log(renderer, "Cannot start GPU capture thread\n");
return 8; goto fail;
} }
renderer.capture_thread_started = true; renderer->capture_thread_started = true;
fprintf(stderr, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, GPU DMA-BUF, XComposite/XShm capture\n", renderers = g_list_prepend(renderers, renderer);
if (idle_source) {
g_source_remove(idle_source);
idle_source = 0;
}
renderer_log(renderer, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, shared profile, GPU DMA-BUF, XComposite/XShm capture\n",
webkit_get_major_version(), webkit_get_major_version(),
webkit_get_minor_version(), webkit_get_micro_version(), renderer.config.width, webkit_get_minor_version(), webkit_get_micro_version(), renderer->config.width,
renderer.config.height, renderer.config.fps); renderer->config.height, renderer->config.fps);
fflush(stderr); return renderer;
g_unix_signal_add(SIGTERM, terminate_renderer, &renderer);
g_unix_signal_add(SIGINT, terminate_renderer, &renderer); fail:
destroy_renderer(renderer);
return NULL;
}
static gboolean accept_client(gint server_fd, GIOCondition condition, gpointer unused)
{
(void)unused;
if (condition & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) {
gtk_main_quit();
return G_SOURCE_REMOVE;
}
for (;;) {
int client_fd = accept4(server_fd, NULL, NULL, SOCK_CLOEXEC);
if (client_fd < 0 && errno == EINTR)
continue;
if (client_fd < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
return G_SOURCE_CONTINUE;
if (client_fd < 0)
return G_SOURCE_CONTINUE;
int descriptors[4] = {-1, -1, -1, -1};
char byte;
struct iovec payload = {.iov_base = &byte, .iov_len = sizeof(byte)};
char control[CMSG_SPACE(sizeof(descriptors))];
struct msghdr message = {0};
message.msg_iov = &payload;
message.msg_iovlen = 1;
message.msg_control = control;
message.msg_controllen = sizeof(control);
ssize_t received = recvmsg(client_fd, &message, 0);
struct cmsghdr *header = received == 1 ? CMSG_FIRSTHDR(&message) : NULL;
if (!header || header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS ||
header->cmsg_len != CMSG_LEN(sizeof(descriptors))) {
close(client_fd);
continue;
}
memcpy(descriptors, CMSG_DATA(header), sizeof(descriptors));
for (size_t index = 0; index < 4; index++) {
int flags = fcntl(descriptors[index], F_GETFD);
if (flags >= 0)
fcntl(descriptors[index], F_SETFD, flags | FD_CLOEXEC);
}
create_renderer(client_fd, descriptors);
}
}
static gboolean terminate_daemon(gpointer unused)
{
(void)unused;
gtk_main_quit();
return G_SOURCE_REMOVE;
}
static socklen_t daemon_address(struct sockaddr_un *address, pid_t owner)
{
memset(address, 0, sizeof(*address));
address->sun_family = AF_UNIX;
g_snprintf(address->sun_path + 1, sizeof(address->sun_path) - 1,
"obs-webkit-browser-%lu-%ld", (unsigned long)getuid(), (long)owner);
return (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 +
strlen(address->sun_path + 1));
}
static int open_daemon_socket(pid_t owner)
{
int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
if (fd < 0)
return -1;
struct sockaddr_un address;
socklen_t length = daemon_address(&address, owner);
if (connect(fd, (struct sockaddr *)&address, length) == 0)
return fd;
close(fd);
return -1;
}
static int daemon_main(pid_t owner)
{
if (setsid() < 0)
return 10;
prepare_webkit();
if (!gtk_init_check(NULL, NULL))
return 5;
int server_fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
if (server_fd < 0)
return 10;
struct sockaddr_un address;
socklen_t address_length = daemon_address(&address, owner);
if (bind(server_fd, (struct sockaddr *)&address, address_length) != 0 ||
listen(server_fd, 32) != 0) {
close(server_fd);
return 10;
}
char *data_directory = g_build_filename(g_get_user_data_dir(),
"obs-webkit-browser", "profile", NULL);
char *cache_directory = g_build_filename(g_get_user_cache_dir(),
"obs-webkit-browser", "profile", NULL);
g_mkdir_with_parents(data_directory, 0700);
g_mkdir_with_parents(cache_directory, 0700);
WebKitWebsiteDataManager *data_manager = webkit_website_data_manager_new(
"base-data-directory", data_directory,
"base-cache-directory", cache_directory, NULL);
webkit_website_data_manager_set_persistent_credential_storage_enabled(data_manager, TRUE);
WebKitCookieManager *cookie_manager =
webkit_website_data_manager_get_cookie_manager(data_manager);
char *cookie_file = g_build_filename(data_directory, "cookies.sqlite", NULL);
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file,
WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
shared_context = webkit_web_context_new_with_website_data_manager(data_manager);
webkit_web_context_set_cache_model(shared_context, WEBKIT_CACHE_MODEL_WEB_BROWSER);
g_object_unref(data_manager);
g_free(cookie_file);
g_free(data_directory);
g_free(cache_directory);
/* The first proxy inherited these descriptors when it forked this daemon.
* Only the SCM_RIGHTS copies accepted above may own an individual view. */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
close(OWB_SHARED_FRAME_FD);
g_unix_fd_add(server_fd, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
accept_client, NULL);
g_unix_signal_add(SIGTERM, terminate_daemon, NULL);
g_unix_signal_add(SIGINT, terminate_daemon, NULL);
gtk_main(); gtk_main();
atomic_store_explicit(&renderer.stopping, true, memory_order_release); while (renderers)
if (renderer.capture_thread_started) destroy_renderer(renderers->data);
pthread_join(renderer.capture_thread, NULL); if (idle_source) {
destroy_x11_capture(&renderer); g_source_remove(idle_source);
munmap(renderer.shared, renderer.shared_size); idle_source = 0;
free(renderer.config.url); }
free(renderer.config.css); g_object_unref(shared_context);
shared_context = NULL;
close(server_fd);
return 0;
}
static bool pass_view_descriptors(int client_fd)
{
int descriptors[4] = {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO,
OWB_SHARED_FRAME_FD};
char byte = 1;
struct iovec payload = {.iov_base = &byte, .iov_len = sizeof(byte)};
char control[CMSG_SPACE(sizeof(descriptors))];
memset(control, 0, sizeof(control));
struct msghdr message = {0};
message.msg_iov = &payload;
message.msg_iovlen = 1;
message.msg_control = control;
message.msg_controllen = sizeof(control);
struct cmsghdr *header = CMSG_FIRSTHDR(&message);
header->cmsg_level = SOL_SOCKET;
header->cmsg_type = SCM_RIGHTS;
header->cmsg_len = CMSG_LEN(sizeof(descriptors));
memcpy(CMSG_DATA(header), descriptors, sizeof(descriptors));
return sendmsg(client_fd, &message, MSG_NOSIGNAL) == 1;
}
int main(void)
{
pid_t owner = getppid();
pid_t launched_daemon = 0;
int client_fd = open_daemon_socket(owner);
if (client_fd < 0) {
launched_daemon = fork();
if (launched_daemon < 0)
return 10;
if (launched_daemon == 0)
_exit(daemon_main(owner));
for (unsigned attempt = 0; attempt < 200 && client_fd < 0; attempt++) {
struct timespec pause = {.tv_nsec = 10000000L};
nanosleep(&pause, NULL);
client_fd = open_daemon_socket(owner);
}
}
if (client_fd < 0) {
int status = 0;
if (launched_daemon > 0 && waitpid(launched_daemon, &status, WNOHANG) == launched_daemon &&
WIFEXITED(status))
return WEXITSTATUS(status);
fprintf(stderr, "Cannot start the shared WebKit renderer process\n");
return 10;
}
if (launched_daemon > 0)
waitpid(launched_daemon, NULL, WNOHANG);
if (!pass_view_descriptors(client_fd)) {
fprintf(stderr, "Cannot attach view to the shared WebKit renderer process\n");
close(client_fd);
return 11;
}
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
close(OWB_SHARED_FRAME_FD);
char ignored;
while (recv(client_fd, &ignored, sizeof(ignored), 0) < 0 && errno == EINTR) {
}
close(client_fd);
return 0; return 0;
} }

View File

@@ -7,7 +7,8 @@ int main(void)
{ {
assert(sizeof(struct owb_config_header) == 36); assert(sizeof(struct owb_config_header) == 36);
assert(sizeof(struct owb_command) == 100); assert(sizeof(struct owb_command) == 100);
assert(sizeof(struct owb_shared_frames) == 40); assert(offsetof(struct owb_shared_frames, pixels) == 40);
assert(owb_shared_size(2, 2) == 72);
assert(offsetof(struct owb_command, text) == 36); assert(offsetof(struct owb_command, text) == 36);
assert(OWB_CONFIG_MAGIC != OWB_COMMAND_MAGIC); assert(OWB_CONFIG_MAGIC != OWB_COMMAND_MAGIC);
return 0; return 0;

View File

@@ -69,7 +69,7 @@ int main(int argc, char **argv)
shared->width = width; shared->width = width;
shared->height = height; shared->height = height;
shared->stride = width * 4; shared->stride = width * 4;
shared->slots = OWB_SHARED_FRAME_SLOTS; shared->slot_count = OWB_SHARED_FRAME_SLOTS;
posix_spawn_file_actions_t actions; posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions); posix_spawn_file_actions_init(&actions);
posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO); posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO);

View File

@@ -84,7 +84,7 @@ int main(int argc, char **argv)
shared->width = width; shared->width = width;
shared->height = height; shared->height = height;
shared->stride = width * 4; shared->stride = width * 4;
shared->slots = OWB_SHARED_FRAME_SLOTS; shared->slot_count = OWB_SHARED_FRAME_SLOTS;
posix_spawn_file_actions_t actions; posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions); posix_spawn_file_actions_init(&actions);
posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO); posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO);

202
tests/shared-profile-test.c Normal file
View File

@@ -0,0 +1,202 @@
#include "protocol.h"
#include <arpa/inet.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/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
extern char **environ;
struct view {
pid_t pid;
int control;
int frames;
int shared_fd;
struct owb_shared_frames *shared;
size_t shared_size;
};
static bool write_all(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 void serve_pages(int server)
{
static const char page[] =
"<!doctype html><style>html,body{margin:0;width:100%;height:100%;background:rgb(2,3,5)}</style>"
"<script>const p=new URL(location.href),t=p.searchParams.get('token'),"
"k='owb-shared-profile-test',b=new BroadcastChannel(k);"
"if(p.pathname==='/writer'){setInterval(()=>{localStorage.setItem(k,t);b.postMessage(t)},100)}"
"else{let heard=false;b.onmessage=e=>{if(e.data===t)heard=true};"
"setInterval(()=>{if(heard&&localStorage.getItem(k)===t)"
"document.body.style.background='rgb(17,231,43)'},50)}</script>";
char response[2048];
int length = snprintf(response, sizeof(response),
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nCache-Control: no-store\r\n"
"Connection: close\r\nContent-Length: %zu\r\n\r\n%s",
strlen(page), page);
for (;;) {
int client = accept(server, NULL, NULL);
if (client < 0 && errno == EINTR)
continue;
if (client < 0)
_exit(0);
char request[1024];
ssize_t ignored = read(client, request, sizeof(request));
(void)ignored;
write_all(client, response, (size_t)length);
close(client);
}
}
static bool start_view(struct view *view, const char *renderer, const char *url)
{
memset(view, 0, sizeof(*view));
view->pid = 0;
view->control = view->frames = view->shared_fd = -1;
int control[2] = {-1, -1};
int frames[2] = {-1, -1};
if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) != 0 || pipe(frames) != 0)
return false;
const uint32_t width = 64, height = 64;
view->shared_size = owb_shared_size(width, height);
view->shared_fd = memfd_create("shared-profile-test", MFD_CLOEXEC);
if (view->shared_fd < 0 || ftruncate(view->shared_fd, (off_t)view->shared_size) != 0)
return false;
view->shared = mmap(NULL, view->shared_size, PROT_READ | PROT_WRITE, MAP_SHARED,
view->shared_fd, 0);
if (view->shared == MAP_FAILED)
return false;
memset(view->shared, 0, sizeof(*view->shared));
view->shared->magic = OWB_SHARED_MAGIC;
view->shared->version = OWB_PROTOCOL_VERSION;
view->shared->width = width;
view->shared->height = height;
view->shared->stride = width * 4;
view->shared->slot_count = 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, view->shared_fd, OWB_SHARED_FRAME_FD);
char *arguments[] = {(char *)renderer, NULL};
int error = posix_spawn(&view->pid, renderer, &actions, NULL, arguments, environ);
posix_spawn_file_actions_destroy(&actions);
close(control[1]);
close(frames[1]);
if (error)
return false;
view->control = control[0];
view->frames = frames[0];
struct owb_config_header config = {
.magic = OWB_CONFIG_MAGIC, .version = OWB_PROTOCOL_VERSION,
.width = width, .height = height, .fps = 20, .transparent = 0,
.hardware_acceleration = 1, .url_length = (uint32_t)strlen(url), .css_length = 0,
};
return write_all(view->control, &config, sizeof(config)) &&
write_all(view->control, url, strlen(url));
}
static int stop_view(struct view *view)
{
int status = 0;
if (view->control >= 0) {
shutdown(view->control, SHUT_RDWR);
close(view->control);
}
if (view->frames >= 0)
close(view->frames);
if (view->shared && view->shared != MAP_FAILED)
munmap(view->shared, view->shared_size);
if (view->shared_fd >= 0)
close(view->shared_fd);
if (view->pid > 0) {
kill(view->pid, SIGTERM);
while (waitpid(view->pid, &status, 0) < 0 && errno == EINTR) {
}
}
return status;
}
int main(int argc, char **argv)
{
if (argc != 2)
return 2;
int server = socket(AF_INET, SOCK_STREAM, 0);
int enabled = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled));
struct sockaddr_in address = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
if (server < 0 || bind(server, (struct sockaddr *)&address, sizeof(address)) != 0 ||
listen(server, 16) != 0)
return 77;
socklen_t address_size = sizeof(address);
if (getsockname(server, (struct sockaddr *)&address, &address_size) != 0)
return 2;
pid_t http_server = fork();
if (http_server == 0)
serve_pages(server);
close(server);
char writer_url[256], reader_url[256];
unsigned port = ntohs(address.sin_port);
snprintf(writer_url, sizeof(writer_url), "http://127.0.0.1:%u/writer?token=%ld", port,
(long)getpid());
snprintf(reader_url, sizeof(reader_url), "http://127.0.0.1:%u/reader?token=%ld", port,
(long)getpid());
struct view writer, reader;
bool started_writer = start_view(&writer, argv[1], writer_url);
bool started_reader = started_writer && start_view(&reader, argv[1], reader_url);
bool shared = false;
for (unsigned attempt = 0; started_reader && attempt < 200; attempt++) {
struct pollfd item = {.fd = reader.frames, .events = POLLIN};
if (poll(&item, 1, 100) <= 0)
continue;
uint8_t slot;
if (read(reader.frames, &slot, sizeof(slot)) != 1 || slot >= OWB_SHARED_FRAME_SLOTS)
continue;
uint8_t *frame = owb_shared_slot(reader.shared, slot);
size_t center = ((size_t)32 * 64 + 32) * 4;
uint8_t *bgra = frame + center;
if (bgra[0] < 55 && bgra[1] > 210 && bgra[2] < 35 && bgra[3] == 255) {
shared = true;
break;
}
}
int reader_status = started_reader ? stop_view(&reader) : 0;
int writer_status = started_writer ? stop_view(&writer) : 0;
kill(http_server, SIGTERM);
waitpid(http_server, NULL, 0);
if (!started_writer || !started_reader)
return 2;
if ((WIFEXITED(reader_status) && WEXITSTATUS(reader_status) == 5) ||
(WIFEXITED(writer_status) && WEXITSTATUS(writer_status) == 5))
return 77;
if (!shared) {
fprintf(stderr, "two renderer views did not share localStorage and BroadcastChannel\n");
return 1;
}
return 0;
}