web docks
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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="Высота"
|
||||||
|
|||||||
@@ -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
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)";
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user