web docks

This commit is contained in:
2026-07-18 11:33:47 +05:00
parent 2c166a40e3
commit d3872d60ef
12 changed files with 900 additions and 11 deletions

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 0.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

View File

@@ -19,6 +19,8 @@ DMA-BUF. Кадры передаются в OBS напрямую как BGRA, б
- shared-memory транспорт кадров без многомегабайтной записи в pipe; - shared-memory транспорт кадров без многомегабайтной записи в pipe;
- мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS; - мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS;
- перезагрузка страницы и остановка renderer-процесса у скрытого source. - перезагрузка страницы и остановка renderer-процесса у скрытого source.
- пользовательские web-панели в меню «Док-панели» OBS с сохранением URL в
коллекции сцен, восстановлением после запуска и интерактивным вводом.
Звук страницы намеренно выключен: добавьте в OBS отдельный Application Audio Capture. Звук страницы намеренно выключен: добавьте в OBS отдельный Application Audio Capture.
Renderer наследует X11/XWayland-дисплей OBS; его нативное окно имеет нулевую opacity, Renderer наследует X11/XWayland-дисплей OBS; его нативное окно имеет нулевую opacity,
@@ -31,7 +33,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
@@ -45,6 +47,9 @@ doas /usr/bin/cmake --install build --prefix /usr
``` ```
После установки перезапустите OBS и добавьте источник **Браузер WebKit (без CEF)**. После установки перезапустите OBS и добавьте источник **Браузер WebKit (без CEF)**.
Для панели откройте **Док-панели → Пользовательские web-панели...**, добавьте
название и URL и нажмите **Применить**. Панель можно скрывать и показывать через
обычное меню доков OBS; правый клик по странице открывает команду перезагрузки.
Если OBS установлен в нестандартный prefix, задайте при конфигурации Если OBS установлен в нестандартный prefix, задайте при конфигурации
`OBS_PLUGIN_DIR` и `OBS_PLUGIN_DATA_DIR`. `OBS_PLUGIN_DIR` и `OBS_PLUGIN_DATA_DIR`.

View File

@@ -1,4 +1,14 @@
WebKitBrowserSource="WebKit Browser (no CEF)" WebKitBrowserSource="WebKit Browser (no CEF)"
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"

View File

@@ -1,4 +1,14 @@
WebKitBrowserSource="Браузер WebKit (без CEF)" WebKitBrowserSource="Браузер WebKit (без CEF)"
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="Высота"

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 \

822
src/docks.cpp Normal file
View File

@@ -0,0 +1,822 @@
#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 <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. */
panels_.clear();
if (menuAction_)
delete menuAction_;
}
void setupFrontend()
{
if (menuAction_ || !QApplication::instance())
return;
auto *main = static_cast<QMainWindow *>(obs_frontend_get_main_window());
if (!main)
return;
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_);
reconcile();
}
private:
static QString dockId(const QString &id) { return QString::fromUtf8(kDockPrefix) + id; }
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_;
};
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;
@@ -602,6 +610,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

@@ -173,7 +173,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)
@@ -386,6 +386,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 +415,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 +429,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 +446,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 +460,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 +476,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;

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);