web docks
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
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)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
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(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
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
|
||||
src/plugin.c
|
||||
src/docks.cpp
|
||||
src/protocol.h
|
||||
)
|
||||
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_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 "")
|
||||
|
||||
add_executable(obs-webkit-renderer
|
||||
|
||||
@@ -19,6 +19,8 @@ DMA-BUF. Кадры передаются в OBS напрямую как BGRA, б
|
||||
- shared-memory транспорт кадров без многомегабайтной записи в pipe;
|
||||
- мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS;
|
||||
- перезагрузка страницы и остановка renderer-процесса у скрытого source.
|
||||
- пользовательские web-панели в меню «Док-панели» OBS с сохранением URL в
|
||||
коллекции сцен, восстановлением после запуска и интерактивным вводом.
|
||||
|
||||
Звук страницы намеренно выключен: добавьте в OBS отдельный Application Audio Capture.
|
||||
Renderer наследует X11/XWayland-дисплей OBS; его нативное окно имеет нулевую opacity,
|
||||
@@ -31,7 +33,7 @@ DRM render node; для него нужен доступ к `/dev/dri/renderD*`.
|
||||
WebKit сам распределяет элементы по GPU-слоям.
|
||||
|
||||
Это рабочий backend, а не замена всех расширенных возможностей штатного obs-browser
|
||||
(DevTools, cookies API, browser docks и DRM здесь пока отсутствуют).
|
||||
(DevTools, cookies API и DRM здесь пока отсутствуют).
|
||||
|
||||
## Сборка на Alpine
|
||||
|
||||
@@ -45,6 +47,9 @@ doas /usr/bin/cmake --install build --prefix /usr
|
||||
```
|
||||
|
||||
После установки перезапустите OBS и добавьте источник **Браузер WebKit (без CEF)**.
|
||||
Для панели откройте **Док-панели → Пользовательские web-панели...**, добавьте
|
||||
название и URL и нажмите **Применить**. Панель можно скрывать и показывать через
|
||||
обычное меню доков OBS; правый клик по странице открывает команду перезагрузки.
|
||||
Если OBS установлен в нестандартный prefix, задайте при конфигурации
|
||||
`OBS_PLUGIN_DIR` и `OBS_PLUGIN_DATA_DIR`.
|
||||
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
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"
|
||||
Width="Width"
|
||||
Height="Height"
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
WebKitBrowserSource="Браузер WebKit (без CEF)"
|
||||
CustomBrowserDocks="Пользовательские web-панели..."
|
||||
CustomBrowserDocksDescription="Добавьте web-панели в меню «Док-панели» OBS. Страницы рендерятся через WebKitGTK."
|
||||
PanelName="Название панели"
|
||||
AddPanel="Добавить"
|
||||
RemovePanel="Удалить"
|
||||
PanelFieldsRequired="Для каждой панели укажите название и URL."
|
||||
PanelNamesUnique="Названия панелей не должны повторяться."
|
||||
PanelLoading="Загрузка web-панели..."
|
||||
PanelRendererError="Не удалось запустить WebKit-рендерер панели."
|
||||
PanelRendererMissing="Вспомогательный WebKit-рендерер не установлен."
|
||||
URL="URL или локальный HTML-файл"
|
||||
Width="Ширина"
|
||||
Height="Высота"
|
||||
|
||||
@@ -15,6 +15,7 @@ fi
|
||||
$ROOT apk add \
|
||||
build-base cmake samurai pkgconf \
|
||||
obs-studio obs-studio-dev \
|
||||
qt6-qtbase-dev \
|
||||
gtk+3.0 gtk+3.0-dev \
|
||||
webkit2gtk-4.1 webkit2gtk-4.1-dev \
|
||||
libx11-dev libxcomposite-dev libxdamage-dev libxext-dev \
|
||||
|
||||
822
src/docks.cpp
Normal file
822
src/docks.cpp
Normal 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;
|
||||
}
|
||||
16
src/plugin.c
16
src/plugin.c
@@ -29,6 +29,14 @@ MODULE_EXPORT const char *obs_module_description(void)
|
||||
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;
|
||||
|
||||
struct browser_source;
|
||||
@@ -295,7 +303,7 @@ static bool start_renderer_locked(struct browser_source *browser)
|
||||
shared->width = browser->width;
|
||||
shared->height = browser->height;
|
||||
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) {
|
||||
blog(LOG_ERROR, "[obs-webkit-browser] IPC setup failed: %s", strerror(errno));
|
||||
goto fail;
|
||||
@@ -602,6 +610,12 @@ static struct obs_source_info browser_info = {
|
||||
bool obs_module_load(void)
|
||||
{
|
||||
obs_register_source(&browser_info);
|
||||
owb_docks_initialize();
|
||||
blog(LOG_INFO, "[obs-webkit-browser] Loaded WebKitGTK browser source");
|
||||
return true;
|
||||
}
|
||||
|
||||
void obs_module_unload(void)
|
||||
{
|
||||
owb_docks_shutdown();
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ struct owb_shared_frames {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t stride;
|
||||
uint32_t slots;
|
||||
uint32_t slot_count;
|
||||
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)
|
||||
@@ -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)
|
||||
{
|
||||
return sizeof(struct owb_shared_frames) +
|
||||
return offsetof(struct owb_shared_frames, pixels) +
|
||||
owb_frame_size(width, height) * OWB_SHARED_FRAME_SLOTS;
|
||||
}
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ static bool map_shared_frames(struct renderer *renderer)
|
||||
renderer->shared->width == renderer->config.width &&
|
||||
renderer->shared->height == renderer->config.height &&
|
||||
renderer->shared->stride == renderer->config.width * 4 &&
|
||||
renderer->shared->slots == OWB_SHARED_FRAME_SLOTS;
|
||||
renderer->shared->slot_count == OWB_SHARED_FRAME_SLOTS;
|
||||
}
|
||||
|
||||
static bool initialize_x11_capture(struct renderer *renderer)
|
||||
@@ -386,6 +386,16 @@ static GdkModifierType modifiers_from_obs(uint32_t modifiers)
|
||||
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)
|
||||
{
|
||||
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->value1) {
|
||||
GdkEvent *event = gdk_event_new(GDK_LEAVE_NOTIFY);
|
||||
set_event_device(event, window, false);
|
||||
event->crossing.window = g_object_ref(window);
|
||||
event->crossing.send_event = TRUE;
|
||||
event->crossing.time = GDK_CURRENT_TIME;
|
||||
@@ -418,6 +429,7 @@ static void dispatch_command(struct renderer *renderer, const struct owb_command
|
||||
return;
|
||||
}
|
||||
GdkEvent *event = gdk_event_new(GDK_MOTION_NOTIFY);
|
||||
set_event_device(event, window, false);
|
||||
event->motion.window = g_object_ref(window);
|
||||
event->motion.send_event = TRUE;
|
||||
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
|
||||
: GDK_BUTTON_PRESS;
|
||||
GdkEvent *event = gdk_event_new(type);
|
||||
set_event_device(event, window, false);
|
||||
event->button.window = g_object_ref(window);
|
||||
event->button.send_event = TRUE;
|
||||
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) {
|
||||
GdkEvent *event = gdk_event_new(GDK_SCROLL);
|
||||
set_event_device(event, window, false);
|
||||
event->scroll.window = g_object_ref(window);
|
||||
event->scroll.send_event = TRUE;
|
||||
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) {
|
||||
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.send_event = TRUE;
|
||||
event->key.time = GDK_CURRENT_TIME;
|
||||
|
||||
@@ -7,7 +7,8 @@ int main(void)
|
||||
{
|
||||
assert(sizeof(struct owb_config_header) == 36);
|
||||
assert(sizeof(struct owb_command) == 100);
|
||||
assert(sizeof(struct owb_shared_frames) == 40);
|
||||
assert(offsetof(struct owb_shared_frames, pixels) == 40);
|
||||
assert(owb_shared_size(2, 2) == 72);
|
||||
assert(offsetof(struct owb_command, text) == 36);
|
||||
assert(OWB_CONFIG_MAGIC != OWB_COMMAND_MAGIC);
|
||||
return 0;
|
||||
|
||||
@@ -69,7 +69,7 @@ int main(int argc, char **argv)
|
||||
shared->width = width;
|
||||
shared->height = height;
|
||||
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_init(&actions);
|
||||
posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO);
|
||||
|
||||
@@ -84,7 +84,7 @@ int main(int argc, char **argv)
|
||||
shared->width = width;
|
||||
shared->height = height;
|
||||
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_init(&actions);
|
||||
posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO);
|
||||
|
||||
Reference in New Issue
Block a user