Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9309e2326 | |||
| eb13c87dd3 | |||
| 07bd12ee9a | |||
| 53161f3f2c | |||
| 8b386b3ab2 | |||
| 3652cfab3c | |||
| d3872d60ef |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
|||||||
/build/
|
/build/
|
||||||
|
/build-*/
|
||||||
|
/dist/
|
||||||
*.o
|
*.o
|
||||||
*.so
|
*.so
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
@@ -1,25 +1,40 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
project(obs-webkit-browser VERSION 0.1.0 LANGUAGES C)
|
project(obs-webkit-browser VERSION 1.2.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)
|
||||||
pkg_check_modules(X11_CAPTURE REQUIRED IMPORTED_TARGET x11 xcomposite xdamage xext)
|
pkg_check_modules(X11_CAPTURE REQUIRED IMPORTED_TARGET x11 xcomposite xdamage xext)
|
||||||
|
pkg_check_modules(PULSE REQUIRED IMPORTED_TARGET libpulse libpulse-simple)
|
||||||
|
|
||||||
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
|
||||||
|
src/pulse-capture.c
|
||||||
|
src/pulse-capture.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
|
||||||
|
PkgConfig::PULSE
|
||||||
|
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
|
||||||
@@ -68,6 +83,10 @@ if(BUILD_TESTING)
|
|||||||
COMMAND renderer-smoke-test $<TARGET_FILE:obs-webkit-renderer> opaque)
|
COMMAND renderer-smoke-test $<TARGET_FILE:obs-webkit-renderer> opaque)
|
||||||
set_tests_properties(renderer-opaque-smoke-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
|
set_tests_properties(renderer-opaque-smoke-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
|
||||||
|
|
||||||
|
add_test(NAME renderer-zoom-smoke-test
|
||||||
|
COMMAND renderer-smoke-test $<TARGET_FILE:obs-webkit-renderer> zoom)
|
||||||
|
set_tests_properties(renderer-zoom-smoke-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
|
||||||
|
|
||||||
add_executable(renderer-fps-test tests/renderer-fps-test.c)
|
add_executable(renderer-fps-test tests/renderer-fps-test.c)
|
||||||
target_compile_features(renderer-fps-test PRIVATE c_std_11)
|
target_compile_features(renderer-fps-test PRIVATE c_std_11)
|
||||||
target_compile_definitions(renderer-fps-test PRIVATE _GNU_SOURCE)
|
target_compile_definitions(renderer-fps-test PRIVATE _GNU_SOURCE)
|
||||||
@@ -76,6 +95,14 @@ if(BUILD_TESTING)
|
|||||||
COMMAND renderer-fps-test $<TARGET_FILE:obs-webkit-renderer>)
|
COMMAND renderer-fps-test $<TARGET_FILE:obs-webkit-renderer>)
|
||||||
set_tests_properties(blur-transition-fps-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
|
set_tests_properties(blur-transition-fps-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
|
||||||
|
|
||||||
|
add_executable(shared-profile-test tests/shared-profile-test.c)
|
||||||
|
target_compile_features(shared-profile-test PRIVATE c_std_11)
|
||||||
|
target_compile_definitions(shared-profile-test PRIVATE _GNU_SOURCE)
|
||||||
|
target_include_directories(shared-profile-test PRIVATE src)
|
||||||
|
add_test(NAME shared-profile-test
|
||||||
|
COMMAND shared-profile-test $<TARGET_FILE:obs-webkit-renderer>)
|
||||||
|
set_tests_properties(shared-profile-test PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77)
|
||||||
|
|
||||||
add_executable(module-load-test tests/module-load-test.c)
|
add_executable(module-load-test tests/module-load-test.c)
|
||||||
target_compile_features(module-load-test PRIVATE c_std_11)
|
target_compile_features(module-load-test PRIVATE c_std_11)
|
||||||
target_link_libraries(module-load-test PRIVATE PkgConfig::OBS)
|
target_link_libraries(module-load-test PRIVATE PkgConfig::OBS)
|
||||||
@@ -83,4 +110,14 @@ if(BUILD_TESTING)
|
|||||||
$<TARGET_FILE:obs-webkit-browser> "${CMAKE_CURRENT_SOURCE_DIR}/data"
|
$<TARGET_FILE:obs-webkit-browser> "${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||||
$<TARGET_FILE:obs-webkit-renderer>)
|
$<TARGET_FILE:obs-webkit-renderer>)
|
||||||
set_tests_properties(module-load-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
|
set_tests_properties(module-load-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
|
||||||
|
|
||||||
|
add_test(NAME module-audio-test COMMAND module-load-test
|
||||||
|
$<TARGET_FILE:obs-webkit-browser> "${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||||
|
$<TARGET_FILE:obs-webkit-renderer> audio-test)
|
||||||
|
set_tests_properties(module-audio-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
|
||||||
|
|
||||||
|
add_test(NAME module-alpha-test COMMAND module-load-test
|
||||||
|
$<TARGET_FILE:obs-webkit-browser> "${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||||
|
$<TARGET_FILE:obs-webkit-renderer> alpha-test)
|
||||||
|
set_tests_properties(module-alpha-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
57
Makefile
Normal file
57
Makefile
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
PROJECT_NAME := obs-webkit-browser
|
||||||
|
VERSION := $(shell sed -n '/^project.* VERSION / { s/^project.* VERSION //; s/ .*//; p; }' CMakeLists.txt)
|
||||||
|
ARCH := $(shell uname -m)
|
||||||
|
|
||||||
|
BUILD_DIR ?= build-release
|
||||||
|
DIST_DIR ?= dist
|
||||||
|
STAGE_DIR ?= $(BUILD_DIR)/stage
|
||||||
|
INSTALL_PREFIX ?= /usr
|
||||||
|
ARCHIVE_NAME := $(PROJECT_NAME)-$(VERSION)-linux-$(ARCH).tar.gz
|
||||||
|
ARCHIVE_PATH := $(abspath $(DIST_DIR)/$(ARCHIVE_NAME))
|
||||||
|
PLUGIN_STAGE_DIR := $(STAGE_DIR)/$(PROJECT_NAME)
|
||||||
|
PLUGIN_BIN_DIR := $(PLUGIN_STAGE_DIR)/bin/64bit
|
||||||
|
PLUGIN_DATA_DIR := $(PLUGIN_STAGE_DIR)/data
|
||||||
|
|
||||||
|
CMAKE ?= $(if $(wildcard /usr/bin/cmake),/usr/bin/cmake,cmake)
|
||||||
|
CTEST ?= $(if $(wildcard /usr/bin/ctest),/usr/bin/ctest,ctest)
|
||||||
|
|
||||||
|
.PHONY: all configure build test install stage archive clean
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
configure:
|
||||||
|
$(CMAKE) -S . -B $(BUILD_DIR) -G Ninja \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_INSTALL_PREFIX=$(INSTALL_PREFIX)
|
||||||
|
|
||||||
|
build: configure
|
||||||
|
$(CMAKE) --build $(BUILD_DIR)
|
||||||
|
|
||||||
|
test: build
|
||||||
|
$(CTEST) --test-dir $(BUILD_DIR) --output-on-failure
|
||||||
|
|
||||||
|
install: build
|
||||||
|
$(CMAKE) --install $(BUILD_DIR) --strip
|
||||||
|
|
||||||
|
stage: test
|
||||||
|
$(CMAKE) -E remove_directory $(STAGE_DIR)
|
||||||
|
$(CMAKE) -E make_directory $(PLUGIN_BIN_DIR)
|
||||||
|
$(CMAKE) -E make_directory $(PLUGIN_DATA_DIR)/locale
|
||||||
|
$(CMAKE) -E copy_if_different \
|
||||||
|
$(BUILD_DIR)/obs-webkit-browser.so $(PLUGIN_BIN_DIR)/obs-webkit-browser.so
|
||||||
|
$(CMAKE) -E copy_if_different \
|
||||||
|
$(BUILD_DIR)/obs-webkit-renderer $(PLUGIN_DATA_DIR)/obs-webkit-renderer
|
||||||
|
$(CMAKE) -E copy_if_different \
|
||||||
|
data/locale/en-US.ini data/locale/ru-RU.ini $(PLUGIN_DATA_DIR)/locale/
|
||||||
|
|
||||||
|
archive: stage
|
||||||
|
$(CMAKE) -E make_directory $(DIST_DIR)
|
||||||
|
cd $(STAGE_DIR) && $(CMAKE) -E tar cfvz $(ARCHIVE_PATH) --format=gnutar $(PROJECT_NAME)
|
||||||
|
$(CMAKE) -E sha256sum $(ARCHIVE_PATH) >$(ARCHIVE_PATH).sha256
|
||||||
|
@echo
|
||||||
|
@echo "Archive: $(ARCHIVE_PATH)"
|
||||||
|
@echo "SHA-256: $(ARCHIVE_PATH).sha256"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
$(CMAKE) -E remove_directory $(BUILD_DIR)
|
||||||
|
$(CMAKE) -E remove_directory $(DIST_DIR)
|
||||||
51
README.md
51
README.md
@@ -12,6 +12,8 @@ DMA-BUF. Кадры передаются в OBS напрямую как BGRA, б
|
|||||||
- HTTP/HTTPS, `data:` URL и локальные HTML-файлы;
|
- HTTP/HTTPS, `data:` URL и локальные HTML-файлы;
|
||||||
- прозрачный фон с alpha-каналом;
|
- прозрачный фон с alpha-каналом;
|
||||||
- пользовательский CSS;
|
- пользовательский CSS;
|
||||||
|
- масштаб страницы 25–500% с сохранением в настройках источника или web-панели;
|
||||||
|
- опциональный захват звука каждого browser source в отдельную полосу Audio Mixer OBS;
|
||||||
- размер 2–8192 px и 1–60 FPS;
|
- размер 2–8192 px и 1–60 FPS;
|
||||||
- аппаратный WebKit compositor через DRM/DMA-BUF (Mesa `radeonsi` на AMD);
|
- аппаратный WebKit compositor через DRM/DMA-BUF (Mesa `radeonsi` на AMD);
|
||||||
- прямой XComposite/XShm-захват скомпозированного GPU-кадра с alpha;
|
- прямой XComposite/XShm-захват скомпозированного GPU-кадра с alpha;
|
||||||
@@ -19,8 +21,22 @@ DMA-BUF. Кадры передаются в OBS напрямую как BGRA, б
|
|||||||
- shared-memory транспорт кадров без многомегабайтной записи в pipe;
|
- shared-memory транспорт кадров без многомегабайтной записи в pipe;
|
||||||
- мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS;
|
- мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS;
|
||||||
- перезагрузка страницы и остановка renderer-процесса у скрытого source.
|
- перезагрузка страницы и остановка renderer-процесса у скрытого source.
|
||||||
|
- пользовательские web-панели в меню «Док-панели» OBS с сохранением URL в
|
||||||
|
коллекции сцен, восстановлением после запуска и интерактивным вводом.
|
||||||
|
- единый постоянный WebKit-профиль для источников на сцене и web-панелей: общие
|
||||||
|
cookies, HTTP cache, localStorage, IndexedDB, service workers и credentials;
|
||||||
|
страницы одного origin взаимодействуют штатными `storage`-событиями и
|
||||||
|
`BroadcastChannel`, как вкладки одного браузера.
|
||||||
|
|
||||||
Звук страницы намеренно выключен: добавьте в OBS отдельный Application Audio Capture.
|
Захват звука по умолчанию выключен. Включите «Захватывать звук браузера» в
|
||||||
|
настройках нужного источника; его громкость появится отдельной полосой в Audio Mixer OBS.
|
||||||
|
Для захвата требуется работающий PulseAudio-совместимый сервер (в том числе
|
||||||
|
PipeWire Pulse) и GStreamer `pulsesink`; скрипт установки зависимостей добавляет
|
||||||
|
необходимый пакет `gst-plugins-good`.
|
||||||
|
Все WebView одной сессии OBS работают в общем renderer-процессе и одном
|
||||||
|
`WebKitWebContext`; маленькие процессы-посредники сохраняют прежнюю независимую
|
||||||
|
жизнь каждого source/dock и изоляцию от OBS. Профиль хранится в стандартных каталогах
|
||||||
|
данных и cache пользователя (`obs-webkit-browser/profile`) и переживает перезапуск OBS.
|
||||||
Renderer наследует X11/XWayland-дисплей OBS; его нативное окно имеет нулевую opacity,
|
Renderer наследует X11/XWayland-дисплей OBS; его нативное окно имеет нулевую opacity,
|
||||||
пустую input shape и не появляется на рабочем столе. Дополнительный display server не
|
пустую input shape и не появляется на рабочем столе. Дополнительный display server не
|
||||||
запускается. WebKit принудительно включает аппаратный compositor и выбирает доступный
|
запускается. WebKit принудительно включает аппаратный compositor и выбирает доступный
|
||||||
@@ -31,7 +47,7 @@ DRM render node; для него нужен доступ к `/dev/dri/renderD*`.
|
|||||||
WebKit сам распределяет элементы по GPU-слоям.
|
WebKit сам распределяет элементы по GPU-слоям.
|
||||||
|
|
||||||
Это рабочий backend, а не замена всех расширенных возможностей штатного obs-browser
|
Это рабочий backend, а не замена всех расширенных возможностей штатного obs-browser
|
||||||
(DevTools, cookies API, browser docks и DRM здесь пока отсутствуют).
|
(DevTools, cookies API и DRM здесь пока отсутствуют).
|
||||||
|
|
||||||
## Сборка на Alpine
|
## Сборка на Alpine
|
||||||
|
|
||||||
@@ -44,10 +60,39 @@ ctest --test-dir build --output-on-failure
|
|||||||
doas /usr/bin/cmake --install build --prefix /usr
|
doas /usr/bin/cmake --install build --prefix /usr
|
||||||
```
|
```
|
||||||
|
|
||||||
После установки перезапустите OBS и добавьте источник **Браузер WebKit (без CEF)**.
|
После установки перезапустите OBS и добавьте источник **Браузер WebKit**.
|
||||||
|
Для панели откройте **Док-панели → Пользовательские web-панели...**, добавьте
|
||||||
|
название и URL и нажмите **Применить**. Панель можно скрывать и показывать через
|
||||||
|
обычное меню доков OBS; правый клик по странице открывает команду перезагрузки.
|
||||||
Если OBS установлен в нестандартный prefix, задайте при конфигурации
|
Если OBS установлен в нестандартный prefix, задайте при конфигурации
|
||||||
`OBS_PLUGIN_DIR` и `OBS_PLUGIN_DATA_DIR`.
|
`OBS_PLUGIN_DIR` и `OBS_PLUGIN_DATA_DIR`.
|
||||||
|
|
||||||
|
## Архив для публикации
|
||||||
|
|
||||||
|
После установки зависимостей используйте отдельные цели `Makefile`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make build
|
||||||
|
make test
|
||||||
|
doas make install
|
||||||
|
make archive
|
||||||
|
```
|
||||||
|
|
||||||
|
`make build` собирает Release-версию, `make test` запускает тесты,
|
||||||
|
`doas make install` устанавливает плагин в `/usr`, а `make archive` запускает
|
||||||
|
сборку и тесты, после чего создаёт пакет без установки файлов в систему.
|
||||||
|
Готовый архив `obs-webkit-browser-<version>-linux-<arch>.tar.gz` и файл с его
|
||||||
|
SHA-256 будут записаны в каталог `dist/`. Внутри нет абсолютных путей: архив
|
||||||
|
содержит только пользовательскую структуру плагина
|
||||||
|
`obs-webkit-browser/{bin/64bit,data}`. Установить её можно напрямую в каталог
|
||||||
|
пользовательских плагинов OBS:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p ~/.config/obs-studio/plugins
|
||||||
|
tar -xzf dist/obs-webkit-browser-<version>-linux-<arch>.tar.gz \
|
||||||
|
-C ~/.config/obs-studio/plugins
|
||||||
|
```
|
||||||
|
|
||||||
## Почему WebKitGTK, а не WPE
|
## Почему WebKitGTK, а не WPE
|
||||||
|
|
||||||
WPE архитектурно подходит лучше всего, но пакет `wpewebkit` отсутствует в актуальном
|
WPE архитектурно подходит лучше всего, но пакет `wpewebkit` отсутствует в актуальном
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
WebKitBrowserSource="WebKit Browser (no CEF)"
|
WebKitBrowserSource="WebKit Browser"
|
||||||
|
CustomBrowserDocks="Custom Web Panels..."
|
||||||
|
CustomBrowserDocksDescription="Add web panels to the OBS Docks menu. Each panel is rendered by WebKitGTK."
|
||||||
|
PanelName="Panel name"
|
||||||
|
AddPanel="Add"
|
||||||
|
RemovePanel="Remove"
|
||||||
|
PanelFieldsRequired="Every panel must have a name and URL."
|
||||||
|
PanelNamesUnique="Panel names must be unique."
|
||||||
|
PanelLoading="Loading web panel..."
|
||||||
|
PanelRendererError="Unable to start the WebKit panel renderer."
|
||||||
|
PanelRendererMissing="The WebKit renderer helper is not installed."
|
||||||
URL="URL or local HTML file"
|
URL="URL or local HTML file"
|
||||||
Width="Width"
|
Width="Width"
|
||||||
Height="Height"
|
Height="Height"
|
||||||
FPS="Frames per second"
|
FPS="Frames per second"
|
||||||
|
PageZoom="Page zoom (%)"
|
||||||
|
CaptureAudio="Capture browser audio"
|
||||||
Transparent="Transparent background"
|
Transparent="Transparent background"
|
||||||
CustomCSS="Custom CSS"
|
CustomCSS="Custom CSS"
|
||||||
ShutdownWhenHidden="Shut down renderer when source is hidden"
|
ShutdownWhenHidden="Shut down renderer when source is hidden"
|
||||||
Reload="Reload page"
|
Reload="Reload page"
|
||||||
|
Refresh="Refresh"
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
WebKitBrowserSource="Браузер WebKit (без CEF)"
|
WebKitBrowserSource="Браузер WebKit"
|
||||||
|
CustomBrowserDocks="Пользовательские web-панели..."
|
||||||
|
CustomBrowserDocksDescription="Добавьте web-панели в меню «Док-панели» OBS. Страницы рендерятся через WebKitGTK."
|
||||||
|
PanelName="Название панели"
|
||||||
|
AddPanel="Добавить"
|
||||||
|
RemovePanel="Удалить"
|
||||||
|
PanelFieldsRequired="Для каждой панели укажите название и URL."
|
||||||
|
PanelNamesUnique="Названия панелей не должны повторяться."
|
||||||
|
PanelLoading="Загрузка web-панели..."
|
||||||
|
PanelRendererError="Не удалось запустить WebKit-рендерер панели."
|
||||||
|
PanelRendererMissing="Вспомогательный WebKit-рендерер не установлен."
|
||||||
URL="URL или локальный HTML-файл"
|
URL="URL или локальный HTML-файл"
|
||||||
Width="Ширина"
|
Width="Ширина"
|
||||||
Height="Высота"
|
Height="Высота"
|
||||||
FPS="Кадров в секунду"
|
FPS="Кадров в секунду"
|
||||||
|
PageZoom="Масштаб страницы (%)"
|
||||||
|
CaptureAudio="Захватывать звук браузера"
|
||||||
Transparent="Прозрачный фон"
|
Transparent="Прозрачный фон"
|
||||||
CustomCSS="Пользовательский CSS"
|
CustomCSS="Пользовательский CSS"
|
||||||
ShutdownWhenHidden="Останавливать рендерер, когда источник скрыт"
|
ShutdownWhenHidden="Останавливать рендерер, когда источник скрыт"
|
||||||
Reload="Перезагрузить страницу"
|
Reload="Перезагрузить страницу"
|
||||||
|
Refresh="Обновить"
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ 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 \
|
||||||
|
gst-plugins-good \
|
||||||
|
pulseaudio-dev \
|
||||||
libx11-dev libxcomposite-dev libxdamage-dev libxext-dev \
|
libx11-dev libxcomposite-dev libxdamage-dev libxext-dev \
|
||||||
mesa-dri-gallium
|
mesa-dri-gallium
|
||||||
|
|||||||
925
src/docks.cpp
Normal file
925
src/docks.cpp
Normal file
@@ -0,0 +1,925 @@
|
|||||||
|
#include <obs/obs-frontend-api.h>
|
||||||
|
#include <obs/obs-interaction.h>
|
||||||
|
#include <obs/obs-module.h>
|
||||||
|
|
||||||
|
#include <QAbstractItemView>
|
||||||
|
#include <QAction>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QBoxLayout>
|
||||||
|
#include <QContextMenuEvent>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QDockWidget>
|
||||||
|
#include <QFocusEvent>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMainWindow>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QMenuBar>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QResizeEvent>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QShowEvent>
|
||||||
|
#include <QSocketNotifier>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#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;
|
||||||
|
int zoomPercent = 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
class WebPanel final : public QWidget {
|
||||||
|
public:
|
||||||
|
explicit WebPanel(QString url, int zoomPercent, QWidget *parent = nullptr)
|
||||||
|
: QWidget(parent), url_(std::move(url)), zoomPercent_(zoomPercent)
|
||||||
|
{
|
||||||
|
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 setConfiguration(const QString &url, int zoomPercent)
|
||||||
|
{
|
||||||
|
zoomPercent = std::clamp(zoomPercent, 25, 500);
|
||||||
|
if (url_ == url && zoomPercent_ == zoomPercent)
|
||||||
|
return;
|
||||||
|
url_ = url;
|
||||||
|
zoomPercent_ = zoomPercent;
|
||||||
|
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.zoom_percent = static_cast<uint32_t>(zoomPercent_);
|
||||||
|
header.capture_audio = 0;
|
||||||
|
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_;
|
||||||
|
int zoomPercent_ = 100;
|
||||||
|
QString status_;
|
||||||
|
QImage frame_;
|
||||||
|
QTimer resizeTimer_;
|
||||||
|
pid_t pid_ = 0;
|
||||||
|
int controlFd_ = -1;
|
||||||
|
int frameFd_ = -1;
|
||||||
|
int logFd_ = -1;
|
||||||
|
int sharedFd_ = -1;
|
||||||
|
owb_shared_frames *shared_ = nullptr;
|
||||||
|
size_t sharedSize_ = 0;
|
||||||
|
int renderWidth_ = 2;
|
||||||
|
int renderHeight_ = 2;
|
||||||
|
QSocketNotifier *frameNotifier_ = nullptr;
|
||||||
|
QSocketNotifier *logNotifier_ = nullptr;
|
||||||
|
QByteArray logBuffer_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DockManager final {
|
||||||
|
public:
|
||||||
|
DockManager()
|
||||||
|
{
|
||||||
|
obs_frontend_add_save_callback(saveCallback, this);
|
||||||
|
obs_frontend_add_preload_callback(preloadCallback, this);
|
||||||
|
obs_frontend_add_event_callback(eventCallback, this);
|
||||||
|
setupFrontend();
|
||||||
|
}
|
||||||
|
|
||||||
|
~DockManager()
|
||||||
|
{
|
||||||
|
/* OBS clears frontend callbacks and owns the dock wrappers before
|
||||||
|
* unloading modules during shutdown. Calling frontend removal APIs
|
||||||
|
* from obs_module_unload is therefore too late. */
|
||||||
|
refreshButtonTimer_.stop();
|
||||||
|
delete refreshButton_;
|
||||||
|
panels_.clear();
|
||||||
|
if (menuAction_)
|
||||||
|
delete menuAction_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupFrontend()
|
||||||
|
{
|
||||||
|
if (!QApplication::instance())
|
||||||
|
return;
|
||||||
|
auto *main = static_cast<QMainWindow *>(obs_frontend_get_main_window());
|
||||||
|
if (!main)
|
||||||
|
return;
|
||||||
|
if (!menuAction_) {
|
||||||
|
menuAction_ = new QAction(text("CustomBrowserDocks"), main);
|
||||||
|
QObject::connect(menuAction_, &QAction::triggered, main, [this] { showEditor(); });
|
||||||
|
if (QMenu *docksMenu = main->findChild<QMenu *>("menuDocks"))
|
||||||
|
docksMenu->addAction(menuAction_);
|
||||||
|
else
|
||||||
|
main->menuBar()->addAction(menuAction_);
|
||||||
|
}
|
||||||
|
setupRefreshButton(main);
|
||||||
|
reconcile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QString dockId(const QString &id) { return QString::fromUtf8(kDockPrefix) + id; }
|
||||||
|
static constexpr const char *kSourceId = "webkit_browser_source";
|
||||||
|
|
||||||
|
obs_source_t *currentContextSource() const
|
||||||
|
{
|
||||||
|
if (!contextSourceLabel_)
|
||||||
|
return nullptr;
|
||||||
|
QByteArray name = contextSourceLabel_->text().toUtf8();
|
||||||
|
return obs_get_source_by_name(name.constData());
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateRefreshButton()
|
||||||
|
{
|
||||||
|
if (!refreshButton_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool visible = false;
|
||||||
|
obs_source_t *source = currentContextSource();
|
||||||
|
if (source) {
|
||||||
|
const char *id = obs_source_get_unversioned_id(source);
|
||||||
|
visible = id && strcmp(id, kSourceId) == 0;
|
||||||
|
obs_source_release(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshButton_->setVisible(visible);
|
||||||
|
if (sourceInteractButton_)
|
||||||
|
refreshButton_->setText(sourceInteractButton_->text().isEmpty() ? QString() : text("Refresh"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupRefreshButton(QMainWindow *main)
|
||||||
|
{
|
||||||
|
if (refreshButton_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
sourceInteractButton_ = main->findChild<QPushButton *>("sourceInteractButton");
|
||||||
|
contextSourceLabel_ = main->findChild<QLabel *>("contextSourceLabel");
|
||||||
|
if (!sourceInteractButton_ || !contextSourceLabel_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto *layout = qobject_cast<QBoxLayout *>(sourceInteractButton_->parentWidget()->layout());
|
||||||
|
if (!layout)
|
||||||
|
return;
|
||||||
|
|
||||||
|
refreshButton_ = new QPushButton(text("Refresh"), sourceInteractButton_->parentWidget());
|
||||||
|
refreshButton_->setObjectName("webkitBrowserRefreshButton");
|
||||||
|
refreshButton_->setMinimumHeight(22);
|
||||||
|
refreshButton_->setToolTip(text("Refresh"));
|
||||||
|
refreshButton_->setIcon(QIcon(":/res/images/refresh.svg"));
|
||||||
|
refreshButton_->setProperty("class", "icon-refresh");
|
||||||
|
layout->insertWidget(layout->indexOf(sourceInteractButton_) + 1, refreshButton_);
|
||||||
|
|
||||||
|
QObject::connect(refreshButton_, &QPushButton::clicked, main, [this] {
|
||||||
|
obs_source_t *source = currentContextSource();
|
||||||
|
if (!source)
|
||||||
|
return;
|
||||||
|
const char *id = obs_source_get_unversioned_id(source);
|
||||||
|
if (id && strcmp(id, kSourceId) == 0) {
|
||||||
|
obs_properties_t *properties = obs_source_properties(source);
|
||||||
|
obs_property_t *reload = properties ? obs_properties_get(properties, "reload") : nullptr;
|
||||||
|
if (reload)
|
||||||
|
obs_property_button_clicked(reload, source);
|
||||||
|
if (properties)
|
||||||
|
obs_properties_destroy(properties);
|
||||||
|
}
|
||||||
|
obs_source_release(source);
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshButtonTimer_.setInterval(100);
|
||||||
|
QObject::connect(&refreshButtonTimer_, &QTimer::timeout, main, [this] { updateRefreshButton(); });
|
||||||
|
refreshButtonTimer_.start();
|
||||||
|
updateRefreshButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void eventCallback(obs_frontend_event event, void *opaque)
|
||||||
|
{
|
||||||
|
auto *manager = static_cast<DockManager *>(opaque);
|
||||||
|
if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING)
|
||||||
|
manager->setupFrontend();
|
||||||
|
else if (event == OBS_FRONTEND_EVENT_EXIT) {
|
||||||
|
manager->panels_.clear();
|
||||||
|
manager->menuAction_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void saveCallback(obs_data_t *saveData, bool saving, void *opaque)
|
||||||
|
{
|
||||||
|
static_cast<DockManager *>(opaque)->saveLoad(saveData, saving);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void preloadCallback(obs_data_t *saveData, bool saving, void *opaque)
|
||||||
|
{
|
||||||
|
if (!saving)
|
||||||
|
static_cast<DockManager *>(opaque)->saveLoad(saveData, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveLoad(obs_data_t *saveData, bool saving)
|
||||||
|
{
|
||||||
|
if (saving) {
|
||||||
|
obs_data_array_t *array = obs_data_array_create();
|
||||||
|
for (const DockDefinition &definition : definitions_) {
|
||||||
|
obs_data_t *item = obs_data_create();
|
||||||
|
obs_data_set_string(item, "id", definition.id.toUtf8().constData());
|
||||||
|
obs_data_set_string(item, "title", definition.title.toUtf8().constData());
|
||||||
|
obs_data_set_string(item, "url", definition.url.toUtf8().constData());
|
||||||
|
obs_data_set_int(item, "zoom", definition.zoomPercent);
|
||||||
|
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")),
|
||||||
|
static_cast<int>(obs_data_get_int(item, "zoom")),
|
||||||
|
};
|
||||||
|
if (definition.zoomPercent == 0)
|
||||||
|
definition.zoomPercent = 100;
|
||||||
|
definition.zoomPercent = std::clamp(definition.zoomPercent, 25, 500);
|
||||||
|
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]->setConfiguration(definition.url,
|
||||||
|
definition.zoomPercent);
|
||||||
|
panels_[definition.id]->setDockTitle(definition.title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto *panel = new WebPanel(definition.url, definition.zoomPercent);
|
||||||
|
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, 3, &dialog);
|
||||||
|
table->setHorizontalHeaderLabels({text("PanelName"), text("URL"), text("PageZoom")});
|
||||||
|
table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||||
|
table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||||
|
table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||||
|
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));
|
||||||
|
auto *zoom = new QSpinBox(table);
|
||||||
|
zoom->setRange(25, 500);
|
||||||
|
zoom->setSingleStep(5);
|
||||||
|
zoom->setSuffix("%");
|
||||||
|
zoom->setValue(definition.zoomPercent);
|
||||||
|
table->setCellWidget(row, 2, zoom);
|
||||||
|
};
|
||||||
|
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://", 100});
|
||||||
|
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();
|
||||||
|
auto *zoom = qobject_cast<QSpinBox *>(table->cellWidget(row, 2));
|
||||||
|
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, zoom ? zoom->value() : 100});
|
||||||
|
}
|
||||||
|
definitions_ = std::move(updated);
|
||||||
|
reconcile();
|
||||||
|
obs_frontend_save();
|
||||||
|
});
|
||||||
|
dialog.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<DockDefinition> definitions_;
|
||||||
|
QHash<QString, QPointer<WebPanel>> panels_;
|
||||||
|
QPointer<QAction> menuAction_;
|
||||||
|
QPointer<QPushButton> sourceInteractButton_;
|
||||||
|
QPointer<QLabel> contextSourceLabel_;
|
||||||
|
QPointer<QPushButton> refreshButton_;
|
||||||
|
QTimer refreshButtonTimer_;
|
||||||
|
};
|
||||||
|
|
||||||
|
DockManager *manager = nullptr;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" void owb_docks_initialize(void)
|
||||||
|
{
|
||||||
|
if (!manager && QApplication::instance())
|
||||||
|
manager = new DockManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void owb_docks_shutdown(void)
|
||||||
|
{
|
||||||
|
delete manager;
|
||||||
|
manager = nullptr;
|
||||||
|
}
|
||||||
114
src/plugin.c
114
src/plugin.c
@@ -1,6 +1,8 @@
|
|||||||
#include <obs/obs-module.h>
|
#include <obs/obs-module.h>
|
||||||
#include <obs/obs-interaction.h>
|
#include <obs/obs-interaction.h>
|
||||||
#include <obs/util/platform.h>
|
#include <obs/util/platform.h>
|
||||||
|
#include <pulse/error.h>
|
||||||
|
#include <pulse/simple.h>
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
|
#include "pulse-capture.h"
|
||||||
|
|
||||||
OBS_DECLARE_MODULE()
|
OBS_DECLARE_MODULE()
|
||||||
OBS_MODULE_USE_DEFAULT_LOCALE("obs-webkit-browser", "en-US")
|
OBS_MODULE_USE_DEFAULT_LOCALE("obs-webkit-browser", "en-US")
|
||||||
@@ -29,6 +32,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;
|
||||||
@@ -43,8 +54,11 @@ struct renderer_process {
|
|||||||
size_t shared_size;
|
size_t shared_size;
|
||||||
pthread_t frame_thread;
|
pthread_t frame_thread;
|
||||||
pthread_t log_thread;
|
pthread_t log_thread;
|
||||||
|
pthread_t audio_thread;
|
||||||
bool frame_thread_started;
|
bool frame_thread_started;
|
||||||
bool log_thread_started;
|
bool log_thread_started;
|
||||||
|
bool audio_thread_started;
|
||||||
|
struct owb_pulse_sink pulse_sink;
|
||||||
atomic_bool stopping;
|
atomic_bool stopping;
|
||||||
uint32_t width;
|
uint32_t width;
|
||||||
uint32_t height;
|
uint32_t height;
|
||||||
@@ -60,6 +74,8 @@ struct browser_source {
|
|||||||
uint32_t width;
|
uint32_t width;
|
||||||
uint32_t height;
|
uint32_t height;
|
||||||
uint32_t fps;
|
uint32_t fps;
|
||||||
|
uint32_t zoom_percent;
|
||||||
|
bool capture_audio;
|
||||||
bool transparent;
|
bool transparent;
|
||||||
bool shutdown_when_hidden;
|
bool shutdown_when_hidden;
|
||||||
bool visible;
|
bool visible;
|
||||||
@@ -187,6 +203,54 @@ static void *log_reader(void *opaque)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void *audio_reader(void *opaque)
|
||||||
|
{
|
||||||
|
struct renderer_process *renderer = opaque;
|
||||||
|
char monitor[OWB_AUDIO_SINK_NAME_SIZE + 16];
|
||||||
|
snprintf(monitor, sizeof(monitor), "%s.monitor", renderer->pulse_sink.name);
|
||||||
|
pa_sample_spec specification = {
|
||||||
|
.format = PA_SAMPLE_FLOAT32NE,
|
||||||
|
.rate = OWB_AUDIO_SAMPLE_RATE,
|
||||||
|
.channels = OWB_AUDIO_CHANNELS,
|
||||||
|
};
|
||||||
|
pa_buffer_attr buffering = {
|
||||||
|
.maxlength = (uint32_t)-1,
|
||||||
|
.tlength = (uint32_t)-1,
|
||||||
|
.prebuf = (uint32_t)-1,
|
||||||
|
.minreq = (uint32_t)-1,
|
||||||
|
.fragsize = OWB_AUDIO_BLOCK_FRAMES * OWB_AUDIO_CHANNELS * sizeof(float),
|
||||||
|
};
|
||||||
|
int pulse_error = 0;
|
||||||
|
pa_simple *stream = pa_simple_new(NULL, "OBS WebKit Browser", PA_STREAM_RECORD,
|
||||||
|
monitor, "Browser source audio", &specification, NULL,
|
||||||
|
&buffering, &pulse_error);
|
||||||
|
if (!stream) {
|
||||||
|
blog(LOG_ERROR, "[obs-webkit-browser] Cannot capture browser audio: %s",
|
||||||
|
pa_strerror(pulse_error));
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
float samples[OWB_AUDIO_BLOCK_FRAMES * OWB_AUDIO_CHANNELS];
|
||||||
|
while (!atomic_load(&renderer->stopping)) {
|
||||||
|
if (pa_simple_read(stream, samples, sizeof(samples), &pulse_error) < 0)
|
||||||
|
break;
|
||||||
|
uint64_t duration = (uint64_t)OWB_AUDIO_BLOCK_FRAMES * 1000000000u /
|
||||||
|
OWB_AUDIO_SAMPLE_RATE;
|
||||||
|
uint64_t now = os_gettime_ns();
|
||||||
|
struct obs_source_audio audio = {
|
||||||
|
.data = {(const uint8_t *)samples},
|
||||||
|
.frames = OWB_AUDIO_BLOCK_FRAMES,
|
||||||
|
.speakers = SPEAKERS_STEREO,
|
||||||
|
.format = AUDIO_FORMAT_FLOAT,
|
||||||
|
.samples_per_sec = OWB_AUDIO_SAMPLE_RATE,
|
||||||
|
.timestamp = now > duration ? now - duration : now,
|
||||||
|
};
|
||||||
|
obs_source_output_audio(renderer->owner->source, &audio);
|
||||||
|
}
|
||||||
|
pa_simple_free(stream);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
static void terminate_renderer_group(pid_t pid)
|
static void terminate_renderer_group(pid_t pid)
|
||||||
{
|
{
|
||||||
if (pid <= 0)
|
if (pid <= 0)
|
||||||
@@ -218,6 +282,8 @@ static void stop_renderer_locked(struct browser_source *browser)
|
|||||||
renderer->control_fd = -1;
|
renderer->control_fd = -1;
|
||||||
}
|
}
|
||||||
terminate_renderer_group(renderer->pid);
|
terminate_renderer_group(renderer->pid);
|
||||||
|
if (renderer->pulse_sink.module_index != UINT32_MAX)
|
||||||
|
owb_pulse_sink_destroy(&renderer->pulse_sink);
|
||||||
if (renderer->frame_fd >= 0) {
|
if (renderer->frame_fd >= 0) {
|
||||||
close(renderer->frame_fd);
|
close(renderer->frame_fd);
|
||||||
renderer->frame_fd = -1;
|
renderer->frame_fd = -1;
|
||||||
@@ -228,6 +294,8 @@ static void stop_renderer_locked(struct browser_source *browser)
|
|||||||
pthread_join(renderer->log_thread, NULL);
|
pthread_join(renderer->log_thread, NULL);
|
||||||
else if (renderer->log_fd >= 0)
|
else if (renderer->log_fd >= 0)
|
||||||
close(renderer->log_fd);
|
close(renderer->log_fd);
|
||||||
|
if (renderer->audio_thread_started)
|
||||||
|
pthread_join(renderer->audio_thread, NULL);
|
||||||
if (renderer->shared && renderer->shared != MAP_FAILED)
|
if (renderer->shared && renderer->shared != MAP_FAILED)
|
||||||
munmap(renderer->shared, renderer->shared_size);
|
munmap(renderer->shared, renderer->shared_size);
|
||||||
if (renderer->shared_fd >= 0)
|
if (renderer->shared_fd >= 0)
|
||||||
@@ -247,11 +315,15 @@ static bool send_initial_config(struct browser_source *browser, struct renderer_
|
|||||||
.width = browser->width,
|
.width = browser->width,
|
||||||
.height = browser->height,
|
.height = browser->height,
|
||||||
.fps = browser->fps,
|
.fps = browser->fps,
|
||||||
|
.zoom_percent = browser->zoom_percent,
|
||||||
|
.capture_audio = browser->capture_audio,
|
||||||
.transparent = browser->transparent,
|
.transparent = browser->transparent,
|
||||||
.hardware_acceleration = true,
|
.hardware_acceleration = true,
|
||||||
.url_length = (uint32_t)url_length,
|
.url_length = (uint32_t)url_length,
|
||||||
.css_length = (uint32_t)css_length,
|
.css_length = (uint32_t)css_length,
|
||||||
};
|
};
|
||||||
|
if (browser->capture_audio)
|
||||||
|
snprintf(header.audio_sink, sizeof(header.audio_sink), "%s", renderer->pulse_sink.name);
|
||||||
return write_all_socket(renderer->control_fd, &header, sizeof(header)) &&
|
return write_all_socket(renderer->control_fd, &header, sizeof(header)) &&
|
||||||
write_all_socket(renderer->control_fd, browser->url, url_length) &&
|
write_all_socket(renderer->control_fd, browser->url, url_length) &&
|
||||||
write_all_socket(renderer->control_fd, browser->css, css_length);
|
write_all_socket(renderer->control_fd, browser->css, css_length);
|
||||||
@@ -277,6 +349,16 @@ static bool start_renderer_locked(struct browser_source *browser)
|
|||||||
int frames[2] = {-1, -1};
|
int frames[2] = {-1, -1};
|
||||||
int logs[2] = {-1, -1};
|
int logs[2] = {-1, -1};
|
||||||
int shared_fd = -1;
|
int shared_fd = -1;
|
||||||
|
struct owb_pulse_sink pulse_sink = {.module_index = UINT32_MAX};
|
||||||
|
if (browser->capture_audio) {
|
||||||
|
char pulse_error[256];
|
||||||
|
if (!owb_pulse_sink_create(&pulse_sink, pulse_error, sizeof(pulse_error))) {
|
||||||
|
blog(LOG_ERROR, "[obs-webkit-browser] Cannot create browser audio sink: %s",
|
||||||
|
pulse_error);
|
||||||
|
bfree(helper);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
struct owb_shared_frames *shared = MAP_FAILED;
|
struct owb_shared_frames *shared = MAP_FAILED;
|
||||||
size_t shared_size = owb_shared_size(browser->width, browser->height);
|
size_t shared_size = owb_shared_size(browser->width, browser->height);
|
||||||
shared_fd = memfd_create("obs-webkit-browser-frames", MFD_CLOEXEC);
|
shared_fd = memfd_create("obs-webkit-browser-frames", MFD_CLOEXEC);
|
||||||
@@ -295,7 +377,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;
|
||||||
@@ -351,6 +433,8 @@ static bool start_renderer_locked(struct browser_source *browser)
|
|||||||
renderer->shared_fd = shared_fd;
|
renderer->shared_fd = shared_fd;
|
||||||
renderer->shared = shared;
|
renderer->shared = shared;
|
||||||
renderer->shared_size = shared_size;
|
renderer->shared_size = shared_size;
|
||||||
|
renderer->pulse_sink = pulse_sink;
|
||||||
|
pulse_sink.module_index = UINT32_MAX;
|
||||||
renderer->width = browser->width;
|
renderer->width = browser->width;
|
||||||
renderer->height = browser->height;
|
renderer->height = browser->height;
|
||||||
renderer->owner = browser;
|
renderer->owner = browser;
|
||||||
@@ -367,6 +451,7 @@ static bool start_renderer_locked(struct browser_source *browser)
|
|||||||
close(renderer->log_fd);
|
close(renderer->log_fd);
|
||||||
munmap(renderer->shared, renderer->shared_size);
|
munmap(renderer->shared, renderer->shared_size);
|
||||||
close(renderer->shared_fd);
|
close(renderer->shared_fd);
|
||||||
|
owb_pulse_sink_destroy(&renderer->pulse_sink);
|
||||||
free(renderer);
|
free(renderer);
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
@@ -374,12 +459,21 @@ static bool start_renderer_locked(struct browser_source *browser)
|
|||||||
renderer->frame_thread_started = true;
|
renderer->frame_thread_started = true;
|
||||||
if (pthread_create(&renderer->log_thread, NULL, log_reader, renderer) == 0)
|
if (pthread_create(&renderer->log_thread, NULL, log_reader, renderer) == 0)
|
||||||
renderer->log_thread_started = true;
|
renderer->log_thread_started = true;
|
||||||
|
if (browser->capture_audio &&
|
||||||
|
pthread_create(&renderer->audio_thread, NULL, audio_reader, renderer) == 0)
|
||||||
|
renderer->audio_thread_started = true;
|
||||||
if (!renderer->frame_thread_started) {
|
if (!renderer->frame_thread_started) {
|
||||||
blog(LOG_ERROR, "[obs-webkit-browser] Cannot start frame reader thread");
|
blog(LOG_ERROR, "[obs-webkit-browser] Cannot start frame reader thread");
|
||||||
browser->renderer = renderer;
|
browser->renderer = renderer;
|
||||||
stop_renderer_locked(browser);
|
stop_renderer_locked(browser);
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
if (browser->capture_audio && !renderer->audio_thread_started) {
|
||||||
|
blog(LOG_ERROR, "[obs-webkit-browser] Cannot start browser audio reader thread");
|
||||||
|
browser->renderer = renderer;
|
||||||
|
stop_renderer_locked(browser);
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
browser->renderer = renderer;
|
browser->renderer = renderer;
|
||||||
blog(LOG_INFO, "[obs-webkit-browser] Started %ux%u@%u renderer", browser->width,
|
blog(LOG_INFO, "[obs-webkit-browser] Started %ux%u@%u renderer", browser->width,
|
||||||
browser->height, browser->fps);
|
browser->height, browser->fps);
|
||||||
@@ -400,6 +494,7 @@ fail:
|
|||||||
munmap(shared, shared_size);
|
munmap(shared, shared_size);
|
||||||
if (shared_fd >= 0)
|
if (shared_fd >= 0)
|
||||||
close(shared_fd);
|
close(shared_fd);
|
||||||
|
owb_pulse_sink_destroy(&pulse_sink);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,11 +521,14 @@ static void browser_update(void *data, obs_data_t *settings)
|
|||||||
browser->width = setting_u32(settings, "width", 2, 8192);
|
browser->width = setting_u32(settings, "width", 2, 8192);
|
||||||
browser->height = setting_u32(settings, "height", 2, 8192);
|
browser->height = setting_u32(settings, "height", 2, 8192);
|
||||||
browser->fps = setting_u32(settings, "fps", 1, 60);
|
browser->fps = setting_u32(settings, "fps", 1, 60);
|
||||||
|
browser->zoom_percent = setting_u32(settings, "zoom", 25, 500);
|
||||||
|
browser->capture_audio = obs_data_get_bool(settings, "capture_audio");
|
||||||
browser->transparent = obs_data_get_bool(settings, "transparent");
|
browser->transparent = obs_data_get_bool(settings, "transparent");
|
||||||
browser->shutdown_when_hidden = obs_data_get_bool(settings, "shutdown_when_hidden");
|
browser->shutdown_when_hidden = obs_data_get_bool(settings, "shutdown_when_hidden");
|
||||||
stop_renderer_locked(browser);
|
stop_renderer_locked(browser);
|
||||||
start_renderer_locked(browser);
|
start_renderer_locked(browser);
|
||||||
pthread_mutex_unlock(&browser->mutex);
|
pthread_mutex_unlock(&browser->mutex);
|
||||||
|
obs_source_set_audio_active(browser->source, browser->capture_audio);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void *browser_create(obs_data_t *settings, obs_source_t *source)
|
static void *browser_create(obs_data_t *settings, obs_source_t *source)
|
||||||
@@ -471,6 +569,8 @@ static void browser_defaults(obs_data_t *settings)
|
|||||||
obs_data_set_default_int(settings, "width", 1280);
|
obs_data_set_default_int(settings, "width", 1280);
|
||||||
obs_data_set_default_int(settings, "height", 720);
|
obs_data_set_default_int(settings, "height", 720);
|
||||||
obs_data_set_default_int(settings, "fps", 30);
|
obs_data_set_default_int(settings, "fps", 30);
|
||||||
|
obs_data_set_default_int(settings, "zoom", 100);
|
||||||
|
obs_data_set_default_bool(settings, "capture_audio", false);
|
||||||
obs_data_set_default_bool(settings, "transparent", true);
|
obs_data_set_default_bool(settings, "transparent", true);
|
||||||
obs_data_set_default_bool(settings, "shutdown_when_hidden", false);
|
obs_data_set_default_bool(settings, "shutdown_when_hidden", false);
|
||||||
}
|
}
|
||||||
@@ -490,6 +590,8 @@ static obs_properties_t *browser_properties(void *data)
|
|||||||
obs_properties_add_int(props, "width", obs_module_text("Width"), 2, 8192, 1);
|
obs_properties_add_int(props, "width", obs_module_text("Width"), 2, 8192, 1);
|
||||||
obs_properties_add_int(props, "height", obs_module_text("Height"), 2, 8192, 1);
|
obs_properties_add_int(props, "height", obs_module_text("Height"), 2, 8192, 1);
|
||||||
obs_properties_add_int(props, "fps", obs_module_text("FPS"), 1, 60, 1);
|
obs_properties_add_int(props, "fps", obs_module_text("FPS"), 1, 60, 1);
|
||||||
|
obs_properties_add_int_slider(props, "zoom", obs_module_text("PageZoom"), 25, 500, 5);
|
||||||
|
obs_properties_add_bool(props, "capture_audio", obs_module_text("CaptureAudio"));
|
||||||
obs_properties_add_bool(props, "transparent", obs_module_text("Transparent"));
|
obs_properties_add_bool(props, "transparent", obs_module_text("Transparent"));
|
||||||
obs_properties_add_text(props, "css", obs_module_text("CustomCSS"), OBS_TEXT_MULTILINE);
|
obs_properties_add_text(props, "css", obs_module_text("CustomCSS"), OBS_TEXT_MULTILINE);
|
||||||
obs_properties_add_bool(props, "shutdown_when_hidden", obs_module_text("ShutdownWhenHidden"));
|
obs_properties_add_bool(props, "shutdown_when_hidden", obs_module_text("ShutdownWhenHidden"));
|
||||||
@@ -581,7 +683,9 @@ static const char *browser_name(void *unused)
|
|||||||
static struct obs_source_info browser_info = {
|
static struct obs_source_info browser_info = {
|
||||||
.id = "webkit_browser_source",
|
.id = "webkit_browser_source",
|
||||||
.type = OBS_SOURCE_TYPE_INPUT,
|
.type = OBS_SOURCE_TYPE_INPUT,
|
||||||
.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_INTERACTION,
|
.icon_type = OBS_ICON_TYPE_BROWSER,
|
||||||
|
.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_AUDIO | OBS_SOURCE_INTERACTION |
|
||||||
|
OBS_SOURCE_SRGB,
|
||||||
.get_name = browser_name,
|
.get_name = browser_name,
|
||||||
.create = browser_create,
|
.create = browser_create,
|
||||||
.destroy = browser_destroy,
|
.destroy = browser_destroy,
|
||||||
@@ -602,6 +706,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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
|
|
||||||
#define OWB_CONFIG_MAGIC 0x4f574243u /* OWBC */
|
#define OWB_CONFIG_MAGIC 0x4f574243u /* OWBC */
|
||||||
#define OWB_COMMAND_MAGIC 0x4f574245u /* OWBE */
|
#define OWB_COMMAND_MAGIC 0x4f574245u /* OWBE */
|
||||||
#define OWB_PROTOCOL_VERSION 2u
|
#define OWB_PROTOCOL_VERSION 4u
|
||||||
#define OWB_MAX_STRING (16u * 1024u * 1024u)
|
#define OWB_MAX_STRING (16u * 1024u * 1024u)
|
||||||
|
#define OWB_AUDIO_SINK_NAME_SIZE 64u
|
||||||
#define OWB_SHARED_MAGIC 0x4f574246u /* OWBF */
|
#define OWB_SHARED_MAGIC 0x4f574246u /* OWBF */
|
||||||
#define OWB_SHARED_FRAME_FD 198
|
#define OWB_SHARED_FRAME_FD 198
|
||||||
#define OWB_SHARED_FRAME_SLOTS 2u
|
#define OWB_SHARED_FRAME_SLOTS 2u
|
||||||
@@ -17,10 +18,13 @@ struct owb_config_header {
|
|||||||
uint32_t width;
|
uint32_t width;
|
||||||
uint32_t height;
|
uint32_t height;
|
||||||
uint32_t fps;
|
uint32_t fps;
|
||||||
|
uint32_t zoom_percent;
|
||||||
|
uint32_t capture_audio;
|
||||||
uint32_t transparent;
|
uint32_t transparent;
|
||||||
uint32_t hardware_acceleration;
|
uint32_t hardware_acceleration;
|
||||||
uint32_t url_length;
|
uint32_t url_length;
|
||||||
uint32_t css_length;
|
uint32_t css_length;
|
||||||
|
char audio_sink[OWB_AUDIO_SINK_NAME_SIZE];
|
||||||
};
|
};
|
||||||
|
|
||||||
struct owb_shared_frames {
|
struct owb_shared_frames {
|
||||||
@@ -29,9 +33,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 +45,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
174
src/pulse-capture.c
Normal file
174
src/pulse-capture.c
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
#include "pulse-capture.h"
|
||||||
|
|
||||||
|
#include <pulse/error.h>
|
||||||
|
#include <pulse/introspect.h>
|
||||||
|
#include <pulse/mainloop.h>
|
||||||
|
#include <pulse/pulseaudio.h>
|
||||||
|
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
struct pulse_connection {
|
||||||
|
pa_mainloop *mainloop;
|
||||||
|
pa_context *context;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct index_result {
|
||||||
|
bool complete;
|
||||||
|
uint32_t index;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct success_result {
|
||||||
|
bool complete;
|
||||||
|
bool success;
|
||||||
|
};
|
||||||
|
|
||||||
|
static atomic_uint sink_serial = 1;
|
||||||
|
|
||||||
|
static uint64_t monotonic_milliseconds(void)
|
||||||
|
{
|
||||||
|
struct timespec now;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
|
return (uint64_t)now.tv_sec * 1000u + (uint64_t)now.tv_nsec / 1000000u;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool pulse_iterate_until(struct pulse_connection *connection, bool *complete,
|
||||||
|
uint64_t deadline)
|
||||||
|
{
|
||||||
|
while (!*complete && monotonic_milliseconds() < deadline) {
|
||||||
|
int result = 0;
|
||||||
|
if (pa_mainloop_iterate(connection->mainloop, 0, &result) < 0)
|
||||||
|
return false;
|
||||||
|
pa_context_state_t state = pa_context_get_state(connection->context);
|
||||||
|
if (!PA_CONTEXT_IS_GOOD(state))
|
||||||
|
return false;
|
||||||
|
struct timespec pause = {.tv_nsec = 10000000L};
|
||||||
|
nanosleep(&pause, NULL);
|
||||||
|
}
|
||||||
|
return *complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool pulse_connection_open(struct pulse_connection *connection, char *error,
|
||||||
|
size_t error_size)
|
||||||
|
{
|
||||||
|
memset(connection, 0, sizeof(*connection));
|
||||||
|
connection->mainloop = pa_mainloop_new();
|
||||||
|
if (!connection->mainloop)
|
||||||
|
goto fail;
|
||||||
|
connection->context = pa_context_new(pa_mainloop_get_api(connection->mainloop),
|
||||||
|
"OBS WebKit Browser");
|
||||||
|
if (!connection->context || pa_context_connect(connection->context, NULL,
|
||||||
|
PA_CONTEXT_NOFLAGS, NULL) < 0)
|
||||||
|
goto fail;
|
||||||
|
|
||||||
|
uint64_t deadline = monotonic_milliseconds() + 3000;
|
||||||
|
while (monotonic_milliseconds() < deadline) {
|
||||||
|
pa_context_state_t state = pa_context_get_state(connection->context);
|
||||||
|
if (state == PA_CONTEXT_READY)
|
||||||
|
return true;
|
||||||
|
if (!PA_CONTEXT_IS_GOOD(state))
|
||||||
|
break;
|
||||||
|
int result = 0;
|
||||||
|
if (pa_mainloop_iterate(connection->mainloop, 0, &result) < 0)
|
||||||
|
break;
|
||||||
|
struct timespec pause = {.tv_nsec = 10000000L};
|
||||||
|
nanosleep(&pause, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
fail:
|
||||||
|
if (error && error_size) {
|
||||||
|
int code = connection->context ? pa_context_errno(connection->context) : PA_ERR_INTERNAL;
|
||||||
|
snprintf(error, error_size, "%s", pa_strerror(code));
|
||||||
|
}
|
||||||
|
if (connection->context) {
|
||||||
|
pa_context_disconnect(connection->context);
|
||||||
|
pa_context_unref(connection->context);
|
||||||
|
}
|
||||||
|
if (connection->mainloop)
|
||||||
|
pa_mainloop_free(connection->mainloop);
|
||||||
|
memset(connection, 0, sizeof(*connection));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void pulse_connection_close(struct pulse_connection *connection)
|
||||||
|
{
|
||||||
|
if (connection->context) {
|
||||||
|
pa_context_disconnect(connection->context);
|
||||||
|
pa_context_unref(connection->context);
|
||||||
|
}
|
||||||
|
if (connection->mainloop)
|
||||||
|
pa_mainloop_free(connection->mainloop);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void module_loaded(pa_context *context, uint32_t index, void *opaque)
|
||||||
|
{
|
||||||
|
(void)context;
|
||||||
|
struct index_result *result = opaque;
|
||||||
|
result->index = index;
|
||||||
|
result->complete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void module_unloaded(pa_context *context, int success, void *opaque)
|
||||||
|
{
|
||||||
|
(void)context;
|
||||||
|
struct success_result *result = opaque;
|
||||||
|
result->success = success != 0;
|
||||||
|
result->complete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool owb_pulse_sink_create(struct owb_pulse_sink *sink, char *error, size_t error_size)
|
||||||
|
{
|
||||||
|
memset(sink, 0, sizeof(*sink));
|
||||||
|
sink->module_index = PA_INVALID_INDEX;
|
||||||
|
unsigned serial = atomic_fetch_add(&sink_serial, 1);
|
||||||
|
snprintf(sink->name, sizeof(sink->name), "owb_%ld_%u", (long)getpid(), serial);
|
||||||
|
|
||||||
|
struct pulse_connection connection;
|
||||||
|
if (!pulse_connection_open(&connection, error, error_size))
|
||||||
|
return false;
|
||||||
|
char arguments[256];
|
||||||
|
snprintf(arguments, sizeof(arguments),
|
||||||
|
"sink_name=%s rate=%u channels=%u sink_properties=device.description=OBS_WebKit_Browser",
|
||||||
|
sink->name, OWB_AUDIO_SAMPLE_RATE, OWB_AUDIO_CHANNELS);
|
||||||
|
struct index_result result = {.index = PA_INVALID_INDEX};
|
||||||
|
pa_operation *operation = pa_context_load_module(connection.context, "module-null-sink",
|
||||||
|
arguments, module_loaded, &result);
|
||||||
|
bool okay = operation && pulse_iterate_until(&connection, &result.complete,
|
||||||
|
monotonic_milliseconds() + 3000) &&
|
||||||
|
result.index != PA_INVALID_INDEX;
|
||||||
|
if (operation)
|
||||||
|
pa_operation_unref(operation);
|
||||||
|
if (!okay && error && error_size)
|
||||||
|
snprintf(error, error_size, "%s", pa_strerror(pa_context_errno(connection.context)));
|
||||||
|
pulse_connection_close(&connection);
|
||||||
|
if (!okay) {
|
||||||
|
sink->name[0] = '\0';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sink->module_index = result.index;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void owb_pulse_sink_destroy(struct owb_pulse_sink *sink)
|
||||||
|
{
|
||||||
|
if (!sink || sink->module_index == PA_INVALID_INDEX)
|
||||||
|
return;
|
||||||
|
struct pulse_connection connection;
|
||||||
|
if (pulse_connection_open(&connection, NULL, 0)) {
|
||||||
|
struct success_result result = {0};
|
||||||
|
pa_operation *operation = pa_context_unload_module(connection.context,
|
||||||
|
sink->module_index,
|
||||||
|
module_unloaded, &result);
|
||||||
|
if (operation) {
|
||||||
|
pulse_iterate_until(&connection, &result.complete,
|
||||||
|
monotonic_milliseconds() + 3000);
|
||||||
|
pa_operation_unref(operation);
|
||||||
|
}
|
||||||
|
pulse_connection_close(&connection);
|
||||||
|
}
|
||||||
|
sink->module_index = PA_INVALID_INDEX;
|
||||||
|
sink->name[0] = '\0';
|
||||||
|
}
|
||||||
19
src/pulse-capture.h
Normal file
19
src/pulse-capture.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "protocol.h"
|
||||||
|
|
||||||
|
#define OWB_AUDIO_SAMPLE_RATE 48000u
|
||||||
|
#define OWB_AUDIO_CHANNELS 2u
|
||||||
|
#define OWB_AUDIO_BLOCK_FRAMES 480u
|
||||||
|
|
||||||
|
struct owb_pulse_sink {
|
||||||
|
uint32_t module_index;
|
||||||
|
char name[OWB_AUDIO_SINK_NAME_SIZE];
|
||||||
|
};
|
||||||
|
|
||||||
|
bool owb_pulse_sink_create(struct owb_pulse_sink *sink, char *error, size_t error_size);
|
||||||
|
void owb_pulse_sink_destroy(struct owb_pulse_sink *sink);
|
||||||
|
|
||||||
549
src/renderer.c
549
src/renderer.c
@@ -14,15 +14,19 @@
|
|||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
#include <stdarg.h>
|
||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <sys/un.h>
|
||||||
#include <sys/ipc.h>
|
#include <sys/ipc.h>
|
||||||
#include <sys/shm.h>
|
#include <sys/shm.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
@@ -31,7 +35,10 @@ struct config {
|
|||||||
uint32_t width;
|
uint32_t width;
|
||||||
uint32_t height;
|
uint32_t height;
|
||||||
uint32_t fps;
|
uint32_t fps;
|
||||||
|
uint32_t zoom_percent;
|
||||||
|
bool capture_audio;
|
||||||
bool transparent;
|
bool transparent;
|
||||||
|
char audio_sink[OWB_AUDIO_SINK_NAME_SIZE];
|
||||||
char *url;
|
char *url;
|
||||||
char *css;
|
char *css;
|
||||||
};
|
};
|
||||||
@@ -40,6 +47,13 @@ struct renderer {
|
|||||||
struct config config;
|
struct config config;
|
||||||
GtkWidget *window;
|
GtkWidget *window;
|
||||||
WebKitWebView *web_view;
|
WebKitWebView *web_view;
|
||||||
|
int control_fd;
|
||||||
|
int frame_fd;
|
||||||
|
int log_fd;
|
||||||
|
int shared_fd;
|
||||||
|
int client_fd;
|
||||||
|
guint control_source;
|
||||||
|
guint client_source;
|
||||||
struct owb_shared_frames *shared;
|
struct owb_shared_frames *shared;
|
||||||
size_t shared_size;
|
size_t shared_size;
|
||||||
uint64_t next_sequence;
|
uint64_t next_sequence;
|
||||||
@@ -59,6 +73,22 @@ struct renderer {
|
|||||||
bool capture_attached;
|
bool capture_attached;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static GList *renderers;
|
||||||
|
static WebKitWebContext *shared_context;
|
||||||
|
static guint idle_source;
|
||||||
|
|
||||||
|
static void destroy_renderer(struct renderer *renderer);
|
||||||
|
|
||||||
|
static void renderer_log(struct renderer *renderer, const char *format, ...)
|
||||||
|
{
|
||||||
|
if (!renderer || renderer->log_fd < 0)
|
||||||
|
return;
|
||||||
|
va_list arguments;
|
||||||
|
va_start(arguments, format);
|
||||||
|
vdprintf(renderer->log_fd, format, arguments);
|
||||||
|
va_end(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
static bool read_exact(int fd, void *data, size_t size)
|
static bool read_exact(int fd, void *data, size_t size)
|
||||||
{
|
{
|
||||||
uint8_t *cursor = data;
|
uint8_t *cursor = data;
|
||||||
@@ -74,27 +104,35 @@ static bool read_exact(int fd, void *data, size_t size)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool read_config(struct config *config)
|
static bool read_config(int fd, struct config *config)
|
||||||
{
|
{
|
||||||
struct owb_config_header header;
|
struct owb_config_header header;
|
||||||
if (!read_exact(STDIN_FILENO, &header, sizeof(header)) || header.magic != OWB_CONFIG_MAGIC ||
|
if (!read_exact(fd, &header, sizeof(header)) || header.magic != OWB_CONFIG_MAGIC ||
|
||||||
header.version != OWB_PROTOCOL_VERSION || header.width < 2 || header.height < 2 ||
|
header.version != OWB_PROTOCOL_VERSION || header.width < 2 || header.height < 2 ||
|
||||||
header.width > 8192 || header.height > 8192 || header.fps < 1 || header.fps > 60 ||
|
header.width > 8192 || header.height > 8192 || header.fps < 1 || header.fps > 60 ||
|
||||||
|
header.zoom_percent < 25 || header.zoom_percent > 500 ||
|
||||||
|
header.capture_audio > 1 ||
|
||||||
|
(header.capture_audio &&
|
||||||
|
(!header.audio_sink[0] ||
|
||||||
|
!memchr(header.audio_sink, '\0', sizeof(header.audio_sink)))) ||
|
||||||
header.url_length > OWB_MAX_STRING || header.css_length > OWB_MAX_STRING)
|
header.url_length > OWB_MAX_STRING || header.css_length > OWB_MAX_STRING)
|
||||||
return false;
|
return false;
|
||||||
config->width = header.width;
|
config->width = header.width;
|
||||||
config->height = header.height;
|
config->height = header.height;
|
||||||
config->fps = header.fps;
|
config->fps = header.fps;
|
||||||
|
config->zoom_percent = header.zoom_percent;
|
||||||
|
config->capture_audio = header.capture_audio != 0;
|
||||||
config->transparent = header.transparent != 0;
|
config->transparent = header.transparent != 0;
|
||||||
/* Protocol v2 retains this legacy flag, but this renderer always enables the
|
memcpy(config->audio_sink, header.audio_sink, sizeof(config->audio_sink));
|
||||||
|
/* The protocol retains this legacy flag, but this renderer always enables the
|
||||||
* WebKit compositor. */
|
* WebKit compositor. */
|
||||||
(void)header.hardware_acceleration;
|
(void)header.hardware_acceleration;
|
||||||
config->url = calloc((size_t)header.url_length + 1, 1);
|
config->url = calloc((size_t)header.url_length + 1, 1);
|
||||||
config->css = calloc((size_t)header.css_length + 1, 1);
|
config->css = calloc((size_t)header.css_length + 1, 1);
|
||||||
if (!config->url || !config->css)
|
if (!config->url || !config->css)
|
||||||
return false;
|
return false;
|
||||||
return read_exact(STDIN_FILENO, config->url, header.url_length) &&
|
return read_exact(fd, config->url, header.url_length) &&
|
||||||
read_exact(STDIN_FILENO, config->css, header.css_length);
|
read_exact(fd, config->css, header.css_length);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char *normalize_uri(const char *value)
|
static char *normalize_uri(const char *value)
|
||||||
@@ -162,10 +200,10 @@ static bool map_shared_frames(struct renderer *renderer)
|
|||||||
{
|
{
|
||||||
struct stat info;
|
struct stat info;
|
||||||
renderer->shared_size = owb_shared_size(renderer->config.width, renderer->config.height);
|
renderer->shared_size = owb_shared_size(renderer->config.width, renderer->config.height);
|
||||||
if (fstat(OWB_SHARED_FRAME_FD, &info) != 0 || (size_t)info.st_size < renderer->shared_size)
|
if (fstat(renderer->shared_fd, &info) != 0 || (size_t)info.st_size < renderer->shared_size)
|
||||||
return false;
|
return false;
|
||||||
renderer->shared = mmap(NULL, renderer->shared_size, PROT_READ | PROT_WRITE, MAP_SHARED,
|
renderer->shared = mmap(NULL, renderer->shared_size, PROT_READ | PROT_WRITE, MAP_SHARED,
|
||||||
OWB_SHARED_FRAME_FD, 0);
|
renderer->shared_fd, 0);
|
||||||
if (renderer->shared == MAP_FAILED)
|
if (renderer->shared == MAP_FAILED)
|
||||||
return false;
|
return false;
|
||||||
return renderer->shared->magic == OWB_SHARED_MAGIC &&
|
return renderer->shared->magic == OWB_SHARED_MAGIC &&
|
||||||
@@ -173,7 +211,7 @@ static bool map_shared_frames(struct renderer *renderer)
|
|||||||
renderer->shared->width == renderer->config.width &&
|
renderer->shared->width == renderer->config.width &&
|
||||||
renderer->shared->height == renderer->config.height &&
|
renderer->shared->height == renderer->config.height &&
|
||||||
renderer->shared->stride == renderer->config.width * 4 &&
|
renderer->shared->stride == renderer->config.width * 4 &&
|
||||||
renderer->shared->slots == OWB_SHARED_FRAME_SLOTS;
|
renderer->shared->slot_count == OWB_SHARED_FRAME_SLOTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool initialize_x11_capture(struct renderer *renderer)
|
static bool initialize_x11_capture(struct renderer *renderer)
|
||||||
@@ -299,7 +337,7 @@ static void publish_frame(struct renderer *renderer, const uint8_t *pixels, int
|
|||||||
}
|
}
|
||||||
__atomic_store_n(&renderer->shared->sequence[slot], complete_sequence, __ATOMIC_RELEASE);
|
__atomic_store_n(&renderer->shared->sequence[slot], complete_sequence, __ATOMIC_RELEASE);
|
||||||
uint8_t notification = (uint8_t)slot;
|
uint8_t notification = (uint8_t)slot;
|
||||||
ssize_t ignored = write(STDOUT_FILENO, ¬ification, sizeof(notification));
|
ssize_t ignored = write(renderer->frame_fd, ¬ification, sizeof(notification));
|
||||||
(void)ignored;
|
(void)ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +390,7 @@ static void *capture_loop(void *opaque)
|
|||||||
None, None);
|
None, None);
|
||||||
bool published = publish_x11_frame(renderer);
|
bool published = publish_x11_frame(renderer);
|
||||||
if (published && !transport_reported && process_uses_drm_render_node()) {
|
if (published && !transport_reported && process_uses_drm_render_node()) {
|
||||||
fprintf(stderr, "DRM render node: active\n");
|
renderer_log(renderer, "DRM render node: active\n");
|
||||||
transport_reported = true;
|
transport_reported = true;
|
||||||
}
|
}
|
||||||
first_frame = false;
|
first_frame = false;
|
||||||
@@ -386,6 +424,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 +453,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 +467,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 +484,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 +498,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 +514,7 @@ static void dispatch_command(struct renderer *renderer, const struct owb_command
|
|||||||
}
|
}
|
||||||
if (command->type == OWB_KEY) {
|
if (command->type == OWB_KEY) {
|
||||||
GdkEvent *event = gdk_event_new(command->value1 ? GDK_KEY_RELEASE : GDK_KEY_PRESS);
|
GdkEvent *event = gdk_event_new(command->value1 ? GDK_KEY_RELEASE : GDK_KEY_PRESS);
|
||||||
|
set_event_device(event, window, true);
|
||||||
event->key.window = g_object_ref(window);
|
event->key.window = g_object_ref(window);
|
||||||
event->key.send_event = TRUE;
|
event->key.send_event = TRUE;
|
||||||
event->key.time = GDK_CURRENT_TIME;
|
event->key.time = GDK_CURRENT_TIME;
|
||||||
@@ -483,7 +536,8 @@ static gboolean control_ready(gint fd, GIOCondition condition, gpointer opaque)
|
|||||||
{
|
{
|
||||||
struct renderer *renderer = opaque;
|
struct renderer *renderer = opaque;
|
||||||
if (condition & (G_IO_HUP | G_IO_ERR)) {
|
if (condition & (G_IO_HUP | G_IO_ERR)) {
|
||||||
gtk_main_quit();
|
renderer->control_source = 0;
|
||||||
|
destroy_renderer(renderer);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -494,7 +548,8 @@ static gboolean control_ready(gint fd, GIOCondition condition, gpointer opaque)
|
|||||||
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||||
return G_SOURCE_CONTINUE;
|
return G_SOURCE_CONTINUE;
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
gtk_main_quit();
|
renderer->control_source = 0;
|
||||||
|
destroy_renderer(renderer);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
renderer->command_used += (size_t)count;
|
renderer->command_used += (size_t)count;
|
||||||
@@ -515,22 +570,14 @@ static gboolean clear_window(GtkWidget *widget, cairo_t *context, gpointer unuse
|
|||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean terminate_renderer(gpointer opaque)
|
|
||||||
{
|
|
||||||
struct renderer *renderer = opaque;
|
|
||||||
atomic_store_explicit(&renderer->stopping, true, memory_order_release);
|
|
||||||
gtk_main_quit();
|
|
||||||
return G_SOURCE_REMOVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void script_message_received(WebKitUserContentManager *manager,
|
static void script_message_received(WebKitUserContentManager *manager,
|
||||||
WebKitJavascriptResult *message, gpointer unused)
|
WebKitJavascriptResult *message, gpointer opaque)
|
||||||
{
|
{
|
||||||
(void)manager;
|
(void)manager;
|
||||||
(void)unused;
|
struct renderer *renderer = opaque;
|
||||||
JSCValue *value = webkit_javascript_result_get_js_value(message);
|
JSCValue *value = webkit_javascript_result_get_js_value(message);
|
||||||
char *text = jsc_value_to_string(value);
|
char *text = jsc_value_to_string(value);
|
||||||
fprintf(stderr, "Page: %s\n", text ? text : "(empty message)");
|
renderer_log(renderer, "Page: %s\n", text ? text : "(empty message)");
|
||||||
g_free(text);
|
g_free(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,34 +586,102 @@ static gboolean load_failed(WebKitWebView *view, WebKitLoadEvent event, const ch
|
|||||||
{
|
{
|
||||||
(void)view;
|
(void)view;
|
||||||
(void)event;
|
(void)event;
|
||||||
(void)unused;
|
struct renderer *renderer = unused;
|
||||||
fprintf(stderr, "Page load failed for %s: %s\n", uri, error->message);
|
renderer_log(renderer, "Page load failed for %s: %s\n", uri, error->message);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void)
|
static gboolean exit_when_idle(gpointer unused)
|
||||||
{
|
{
|
||||||
struct renderer renderer = {0};
|
(void)unused;
|
||||||
atomic_init(&renderer.stopping, false);
|
idle_source = 0;
|
||||||
if (!read_config(&renderer.config)) {
|
if (!renderers)
|
||||||
fprintf(stderr, "Invalid renderer configuration\n");
|
gtk_main_quit();
|
||||||
return 2;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
prepare_webkit();
|
|
||||||
if (!map_shared_frames(&renderer)) {
|
static void destroy_renderer(struct renderer *renderer)
|
||||||
fprintf(stderr, "Cannot map shared frame transport\n");
|
{
|
||||||
return 4;
|
if (!renderer)
|
||||||
|
return;
|
||||||
|
renderers = g_list_remove(renderers, renderer);
|
||||||
|
if (renderer->control_source)
|
||||||
|
g_source_remove(renderer->control_source);
|
||||||
|
if (renderer->client_source)
|
||||||
|
g_source_remove(renderer->client_source);
|
||||||
|
atomic_store_explicit(&renderer->stopping, true, memory_order_release);
|
||||||
|
if (renderer->capture_thread_started)
|
||||||
|
pthread_join(renderer->capture_thread, NULL);
|
||||||
|
destroy_x11_capture(renderer);
|
||||||
|
if (renderer->window)
|
||||||
|
gtk_widget_destroy(renderer->window);
|
||||||
|
if (renderer->shared && renderer->shared != MAP_FAILED)
|
||||||
|
munmap(renderer->shared, renderer->shared_size);
|
||||||
|
if (renderer->control_fd >= 0)
|
||||||
|
close(renderer->control_fd);
|
||||||
|
if (renderer->frame_fd >= 0)
|
||||||
|
close(renderer->frame_fd);
|
||||||
|
if (renderer->log_fd >= 0)
|
||||||
|
close(renderer->log_fd);
|
||||||
|
if (renderer->shared_fd >= 0)
|
||||||
|
close(renderer->shared_fd);
|
||||||
|
if (renderer->client_fd >= 0)
|
||||||
|
close(renderer->client_fd);
|
||||||
|
free(renderer->config.url);
|
||||||
|
free(renderer->config.css);
|
||||||
|
free(renderer);
|
||||||
|
if (!renderers && !idle_source)
|
||||||
|
idle_source = g_timeout_add_seconds(2, exit_when_idle, NULL);
|
||||||
}
|
}
|
||||||
if (!gtk_init_check(NULL, NULL)) {
|
|
||||||
fprintf(stderr, "GTK could not connect to the OBS display\n");
|
static gboolean client_ready(gint fd, GIOCondition condition, gpointer opaque)
|
||||||
return 5;
|
{
|
||||||
|
struct renderer *renderer = opaque;
|
||||||
|
if (condition & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) {
|
||||||
|
renderer->client_source = 0;
|
||||||
|
destroy_renderer(renderer);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
char ignored[16];
|
||||||
|
ssize_t count = recv(fd, ignored, sizeof(ignored), MSG_DONTWAIT);
|
||||||
|
if (count == 0) {
|
||||||
|
renderer->client_source = 0;
|
||||||
|
destroy_renderer(renderer);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct renderer *create_renderer(int client_fd, const int descriptors[4])
|
||||||
|
{
|
||||||
|
struct renderer *renderer = calloc(1, sizeof(*renderer));
|
||||||
|
if (!renderer) {
|
||||||
|
for (size_t index = 0; index < 4; index++)
|
||||||
|
close(descriptors[index]);
|
||||||
|
close(client_fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
renderer->control_fd = descriptors[0];
|
||||||
|
renderer->frame_fd = descriptors[1];
|
||||||
|
renderer->log_fd = descriptors[2];
|
||||||
|
renderer->shared_fd = descriptors[3];
|
||||||
|
renderer->client_fd = client_fd;
|
||||||
|
renderer->capture_shm.shmid = -1;
|
||||||
|
atomic_init(&renderer->stopping, false);
|
||||||
|
if (!read_config(renderer->control_fd, &renderer->config)) {
|
||||||
|
renderer_log(renderer, "Invalid renderer configuration\n");
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
if (!map_shared_frames(renderer)) {
|
||||||
|
renderer_log(renderer, "Cannot map shared frame transport\n");
|
||||||
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
WebKitUserContentManager *manager = webkit_user_content_manager_new();
|
WebKitUserContentManager *manager = webkit_user_content_manager_new();
|
||||||
g_signal_connect(manager, "script-message-received::owbLog",
|
g_signal_connect(manager, "script-message-received::owbLog",
|
||||||
G_CALLBACK(script_message_received), NULL);
|
G_CALLBACK(script_message_received), renderer);
|
||||||
if (!webkit_user_content_manager_register_script_message_handler(manager, "owbLog"))
|
if (!webkit_user_content_manager_register_script_message_handler(manager, "owbLog"))
|
||||||
fprintf(stderr, "Cannot register page diagnostic message handler\n");
|
renderer_log(renderer, "Cannot register page diagnostic message handler\n");
|
||||||
static const char error_reporter[] =
|
static const char error_reporter[] =
|
||||||
"(()=>{const p=(k,v)=>{try{window.webkit.messageHandlers.owbLog.postMessage(k+': '+v)}catch{}};"
|
"(()=>{const p=(k,v)=>{try{window.webkit.messageHandlers.owbLog.postMessage(k+': '+v)}catch{}};"
|
||||||
"addEventListener('error',e=>p('error',e.message+' @ '+e.filename+':'+e.lineno));"
|
"addEventListener('error',e=>p('error',e.message+' @ '+e.filename+':'+e.lineno));"
|
||||||
@@ -577,7 +692,7 @@ int main(void)
|
|||||||
NULL, NULL);
|
NULL, NULL);
|
||||||
webkit_user_content_manager_add_script(manager, reporter);
|
webkit_user_content_manager_add_script(manager, reporter);
|
||||||
webkit_user_script_unref(reporter);
|
webkit_user_script_unref(reporter);
|
||||||
if (renderer.config.transparent) {
|
if (renderer->config.transparent) {
|
||||||
static const char transparent_page_css[] =
|
static const char transparent_page_css[] =
|
||||||
"html,body{background:transparent!important}";
|
"html,body{background:transparent!important}";
|
||||||
WebKitUserStyleSheet *transparent_sheet = webkit_user_style_sheet_new(
|
WebKitUserStyleSheet *transparent_sheet = webkit_user_style_sheet_new(
|
||||||
@@ -586,96 +701,330 @@ int main(void)
|
|||||||
webkit_user_content_manager_add_style_sheet(manager, transparent_sheet);
|
webkit_user_content_manager_add_style_sheet(manager, transparent_sheet);
|
||||||
webkit_user_style_sheet_unref(transparent_sheet);
|
webkit_user_style_sheet_unref(transparent_sheet);
|
||||||
}
|
}
|
||||||
if (*renderer.config.css) {
|
if (*renderer->config.css) {
|
||||||
WebKitUserStyleSheet *sheet = webkit_user_style_sheet_new(renderer.config.css,
|
WebKitUserStyleSheet *sheet = webkit_user_style_sheet_new(renderer->config.css,
|
||||||
WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_STYLE_LEVEL_USER, NULL, NULL);
|
WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_STYLE_LEVEL_USER, NULL, NULL);
|
||||||
webkit_user_content_manager_add_style_sheet(manager, sheet);
|
webkit_user_content_manager_add_style_sheet(manager, sheet);
|
||||||
webkit_user_style_sheet_unref(sheet);
|
webkit_user_style_sheet_unref(sheet);
|
||||||
}
|
}
|
||||||
renderer.web_view = WEBKIT_WEB_VIEW(webkit_web_view_new_with_user_content_manager(manager));
|
WebKitWebsitePolicies *policies = webkit_website_policies_new_with_policies(
|
||||||
g_signal_connect(renderer.web_view, "load-failed", G_CALLBACK(load_failed), NULL);
|
"autoplay", WEBKIT_AUTOPLAY_ALLOW, NULL);
|
||||||
|
renderer->web_view = WEBKIT_WEB_VIEW(g_object_new(
|
||||||
|
WEBKIT_TYPE_WEB_VIEW, "web-context", shared_context,
|
||||||
|
"user-content-manager", manager, "website-policies", policies, NULL));
|
||||||
|
g_object_unref(policies);
|
||||||
|
g_signal_connect(renderer->web_view, "load-failed", G_CALLBACK(load_failed), renderer);
|
||||||
g_object_unref(manager);
|
g_object_unref(manager);
|
||||||
WebKitSettings *settings = webkit_web_view_get_settings(renderer.web_view);
|
WebKitSettings *settings = webkit_web_view_get_settings(renderer->web_view);
|
||||||
|
webkit_settings_set_enable_html5_local_storage(settings, TRUE);
|
||||||
webkit_settings_set_enable_webgl(settings, TRUE);
|
webkit_settings_set_enable_webgl(settings, TRUE);
|
||||||
webkit_settings_set_enable_page_cache(settings, TRUE);
|
webkit_settings_set_enable_page_cache(settings, TRUE);
|
||||||
webkit_settings_set_enable_smooth_scrolling(settings, TRUE);
|
webkit_settings_set_enable_smooth_scrolling(settings, TRUE);
|
||||||
webkit_settings_set_media_playback_requires_user_gesture(settings, FALSE);
|
webkit_settings_set_media_playback_requires_user_gesture(settings, FALSE);
|
||||||
webkit_settings_set_hardware_acceleration_policy(settings,
|
webkit_settings_set_hardware_acceleration_policy(settings,
|
||||||
WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
|
WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
|
||||||
webkit_web_view_set_is_muted(renderer.web_view, TRUE);
|
webkit_web_view_set_zoom_level(renderer->web_view,
|
||||||
GdkRGBA background = renderer.config.transparent ? (GdkRGBA){0, 0, 0, 0} : (GdkRGBA){1, 1, 1, 1};
|
(double)renderer->config.zoom_percent / 100.0);
|
||||||
webkit_web_view_set_background_color(renderer.web_view, &background);
|
webkit_web_view_set_is_muted(renderer->web_view, !renderer->config.capture_audio);
|
||||||
|
GdkRGBA background = renderer->config.transparent ? (GdkRGBA){0, 0, 0, 0} : (GdkRGBA){1, 1, 1, 1};
|
||||||
|
webkit_web_view_set_background_color(renderer->web_view, &background);
|
||||||
|
|
||||||
renderer.window = gtk_window_new(GTK_WINDOW_POPUP);
|
renderer->window = gtk_window_new(GTK_WINDOW_POPUP);
|
||||||
GdkScreen *screen = gdk_screen_get_default();
|
GdkScreen *screen = gdk_screen_get_default();
|
||||||
GdkVisual *visual = renderer.config.transparent ? gdk_screen_get_rgba_visual(screen)
|
GdkVisual *visual = renderer->config.transparent ? gdk_screen_get_rgba_visual(screen)
|
||||||
: gdk_screen_get_system_visual(screen);
|
: gdk_screen_get_system_visual(screen);
|
||||||
if (visual)
|
if (visual)
|
||||||
gtk_widget_set_visual(renderer.window, visual);
|
gtk_widget_set_visual(renderer->window, visual);
|
||||||
gtk_widget_set_app_paintable(renderer.window, renderer.config.transparent);
|
gtk_widget_set_app_paintable(renderer->window, renderer->config.transparent);
|
||||||
if (renderer.config.transparent)
|
if (renderer->config.transparent)
|
||||||
g_signal_connect(renderer.window, "draw", G_CALLBACK(clear_window), NULL);
|
g_signal_connect(renderer->window, "draw", G_CALLBACK(clear_window), NULL);
|
||||||
gtk_window_set_decorated(GTK_WINDOW(renderer.window), FALSE);
|
gtk_window_set_decorated(GTK_WINDOW(renderer->window), FALSE);
|
||||||
gtk_window_set_resizable(GTK_WINDOW(renderer.window), FALSE);
|
gtk_window_set_resizable(GTK_WINDOW(renderer->window), FALSE);
|
||||||
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(renderer.window), TRUE);
|
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(renderer->window), TRUE);
|
||||||
gtk_window_set_skip_pager_hint(GTK_WINDOW(renderer.window), TRUE);
|
gtk_window_set_skip_pager_hint(GTK_WINDOW(renderer->window), TRUE);
|
||||||
gtk_window_set_accept_focus(GTK_WINDOW(renderer.window), FALSE);
|
gtk_window_set_accept_focus(GTK_WINDOW(renderer->window), FALSE);
|
||||||
gtk_window_set_focus_on_map(GTK_WINDOW(renderer.window), FALSE);
|
gtk_window_set_focus_on_map(GTK_WINDOW(renderer->window), FALSE);
|
||||||
gtk_window_move(GTK_WINDOW(renderer.window), 0, 0);
|
gtk_window_move(GTK_WINDOW(renderer->window), 0, 0);
|
||||||
gtk_window_set_default_size(GTK_WINDOW(renderer.window), (int)renderer.config.width,
|
gtk_window_set_default_size(GTK_WINDOW(renderer->window), (int)renderer->config.width,
|
||||||
(int)renderer.config.height);
|
(int)renderer->config.height);
|
||||||
gtk_widget_set_size_request(GTK_WIDGET(renderer.web_view), (int)renderer.config.width,
|
gtk_widget_set_size_request(GTK_WIDGET(renderer->web_view), (int)renderer->config.width,
|
||||||
(int)renderer.config.height);
|
(int)renderer->config.height);
|
||||||
gtk_container_add(GTK_CONTAINER(renderer.window), GTK_WIDGET(renderer.web_view));
|
gtk_container_add(GTK_CONTAINER(renderer->window), GTK_WIDGET(renderer->web_view));
|
||||||
gtk_widget_realize(renderer.window);
|
gtk_widget_realize(renderer->window);
|
||||||
GdkWindow *renderer_window = gtk_widget_get_window(renderer.window);
|
GdkWindow *renderer_window = gtk_widget_get_window(renderer->window);
|
||||||
if (!renderer_window || gdk_window_get_window_type(renderer_window) == GDK_WINDOW_OFFSCREEN) {
|
if (!renderer_window || gdk_window_get_window_type(renderer_window) == GDK_WINDOW_OFFSCREEN) {
|
||||||
fprintf(stderr, "GTK did not create the native window required for GPU rendering\n");
|
renderer_log(renderer, "GTK did not create the native window required for GPU rendering\n");
|
||||||
return 6;
|
goto fail;
|
||||||
}
|
}
|
||||||
cairo_region_t *empty_input = cairo_region_create();
|
cairo_region_t *empty_input = cairo_region_create();
|
||||||
gdk_window_input_shape_combine_region(renderer_window, empty_input, 0, 0);
|
gdk_window_input_shape_combine_region(renderer_window, empty_input, 0, 0);
|
||||||
cairo_region_destroy(empty_input);
|
cairo_region_destroy(empty_input);
|
||||||
gdk_window_set_opacity(renderer_window, 0.0);
|
gdk_window_set_opacity(renderer_window, 0.0);
|
||||||
gtk_widget_show_all(renderer.window);
|
gtk_widget_show_all(renderer->window);
|
||||||
while (gtk_events_pending())
|
while (gtk_events_pending())
|
||||||
gtk_main_iteration();
|
gtk_main_iteration();
|
||||||
if (!initialize_x11_capture(&renderer)) {
|
if (!initialize_x11_capture(renderer)) {
|
||||||
fprintf(stderr, "Cannot initialize XComposite/XDamage/XShm GPU capture\n");
|
renderer_log(renderer, "Cannot initialize XComposite/XDamage/XShm GPU capture\n");
|
||||||
return 7;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
int output_flags = fcntl(STDOUT_FILENO, F_GETFL, 0);
|
int output_flags = fcntl(renderer->frame_fd, F_GETFL, 0);
|
||||||
if (output_flags >= 0)
|
if (output_flags >= 0)
|
||||||
fcntl(STDOUT_FILENO, F_SETFL, output_flags | O_NONBLOCK);
|
fcntl(renderer->frame_fd, F_SETFL, output_flags | O_NONBLOCK);
|
||||||
|
|
||||||
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
|
int flags = fcntl(renderer->control_fd, F_GETFL, 0);
|
||||||
if (flags >= 0)
|
if (flags >= 0)
|
||||||
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
|
fcntl(renderer->control_fd, F_SETFL, flags | O_NONBLOCK);
|
||||||
g_unix_fd_add(STDIN_FILENO, G_IO_IN | G_IO_HUP | G_IO_ERR, control_ready, &renderer);
|
renderer->control_source = g_unix_fd_add(renderer->control_fd,
|
||||||
char *uri = normalize_uri(renderer.config.url);
|
G_IO_IN | G_IO_HUP | G_IO_ERR, control_ready, renderer);
|
||||||
webkit_web_view_load_uri(renderer.web_view, uri);
|
renderer->client_source = g_unix_fd_add(renderer->client_fd,
|
||||||
g_free(uri);
|
G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, client_ready, renderer);
|
||||||
if (pthread_create(&renderer.capture_thread, NULL, capture_loop, &renderer) != 0) {
|
char *uri = normalize_uri(renderer->config.url);
|
||||||
fprintf(stderr, "Cannot start GPU capture thread\n");
|
char *previous_pulse_sink = g_strdup(g_getenv("PULSE_SINK"));
|
||||||
return 8;
|
char *previous_feature_rank = g_strdup(g_getenv("GST_PLUGIN_FEATURE_RANK"));
|
||||||
|
if (renderer->config.capture_audio) {
|
||||||
|
g_setenv("PULSE_SINK", renderer->config.audio_sink, TRUE);
|
||||||
|
char *audio_feature_rank = previous_feature_rank && *previous_feature_rank
|
||||||
|
? g_strdup_printf("pipewiresink:NONE,pulsesink:MAX,%s", previous_feature_rank)
|
||||||
|
: g_strdup("pipewiresink:NONE,pulsesink:MAX");
|
||||||
|
g_setenv("GST_PLUGIN_FEATURE_RANK", audio_feature_rank, TRUE);
|
||||||
|
g_free(audio_feature_rank);
|
||||||
}
|
}
|
||||||
renderer.capture_thread_started = true;
|
webkit_web_view_load_uri(renderer->web_view, uri);
|
||||||
fprintf(stderr, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, GPU DMA-BUF, XComposite/XShm capture\n",
|
if (previous_pulse_sink) {
|
||||||
|
g_setenv("PULSE_SINK", previous_pulse_sink, TRUE);
|
||||||
|
g_free(previous_pulse_sink);
|
||||||
|
} else {
|
||||||
|
g_unsetenv("PULSE_SINK");
|
||||||
|
}
|
||||||
|
if (previous_feature_rank) {
|
||||||
|
g_setenv("GST_PLUGIN_FEATURE_RANK", previous_feature_rank, TRUE);
|
||||||
|
g_free(previous_feature_rank);
|
||||||
|
} else {
|
||||||
|
g_unsetenv("GST_PLUGIN_FEATURE_RANK");
|
||||||
|
}
|
||||||
|
g_free(uri);
|
||||||
|
if (pthread_create(&renderer->capture_thread, NULL, capture_loop, renderer) != 0) {
|
||||||
|
renderer_log(renderer, "Cannot start GPU capture thread\n");
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
renderer->capture_thread_started = true;
|
||||||
|
renderers = g_list_prepend(renderers, renderer);
|
||||||
|
if (idle_source) {
|
||||||
|
g_source_remove(idle_source);
|
||||||
|
idle_source = 0;
|
||||||
|
}
|
||||||
|
renderer_log(renderer, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, zoom %u%%, shared profile, GPU DMA-BUF, XComposite/XShm capture\n",
|
||||||
webkit_get_major_version(),
|
webkit_get_major_version(),
|
||||||
webkit_get_minor_version(), webkit_get_micro_version(), renderer.config.width,
|
webkit_get_minor_version(), webkit_get_micro_version(), renderer->config.width,
|
||||||
renderer.config.height, renderer.config.fps);
|
renderer->config.height, renderer->config.fps, renderer->config.zoom_percent);
|
||||||
fflush(stderr);
|
return renderer;
|
||||||
g_unix_signal_add(SIGTERM, terminate_renderer, &renderer);
|
|
||||||
g_unix_signal_add(SIGINT, terminate_renderer, &renderer);
|
fail:
|
||||||
|
destroy_renderer(renderer);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean accept_client(gint server_fd, GIOCondition condition, gpointer unused)
|
||||||
|
{
|
||||||
|
(void)unused;
|
||||||
|
if (condition & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) {
|
||||||
|
gtk_main_quit();
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
int client_fd = accept4(server_fd, NULL, NULL, SOCK_CLOEXEC);
|
||||||
|
if (client_fd < 0 && errno == EINTR)
|
||||||
|
continue;
|
||||||
|
if (client_fd < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
if (client_fd < 0)
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
|
||||||
|
int descriptors[4] = {-1, -1, -1, -1};
|
||||||
|
char byte;
|
||||||
|
struct iovec payload = {.iov_base = &byte, .iov_len = sizeof(byte)};
|
||||||
|
char control[CMSG_SPACE(sizeof(descriptors))];
|
||||||
|
struct msghdr message = {0};
|
||||||
|
message.msg_iov = &payload;
|
||||||
|
message.msg_iovlen = 1;
|
||||||
|
message.msg_control = control;
|
||||||
|
message.msg_controllen = sizeof(control);
|
||||||
|
ssize_t received = recvmsg(client_fd, &message, 0);
|
||||||
|
struct cmsghdr *header = received == 1 ? CMSG_FIRSTHDR(&message) : NULL;
|
||||||
|
if (!header || header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS ||
|
||||||
|
header->cmsg_len != CMSG_LEN(sizeof(descriptors))) {
|
||||||
|
close(client_fd);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
memcpy(descriptors, CMSG_DATA(header), sizeof(descriptors));
|
||||||
|
for (size_t index = 0; index < 4; index++) {
|
||||||
|
int flags = fcntl(descriptors[index], F_GETFD);
|
||||||
|
if (flags >= 0)
|
||||||
|
fcntl(descriptors[index], F_SETFD, flags | FD_CLOEXEC);
|
||||||
|
}
|
||||||
|
create_renderer(client_fd, descriptors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean terminate_daemon(gpointer unused)
|
||||||
|
{
|
||||||
|
(void)unused;
|
||||||
|
gtk_main_quit();
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static socklen_t daemon_address(struct sockaddr_un *address, pid_t owner)
|
||||||
|
{
|
||||||
|
memset(address, 0, sizeof(*address));
|
||||||
|
address->sun_family = AF_UNIX;
|
||||||
|
g_snprintf(address->sun_path + 1, sizeof(address->sun_path) - 1,
|
||||||
|
"obs-webkit-browser-%lu-%ld", (unsigned long)getuid(), (long)owner);
|
||||||
|
return (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 +
|
||||||
|
strlen(address->sun_path + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int open_daemon_socket(pid_t owner)
|
||||||
|
{
|
||||||
|
int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
|
||||||
|
if (fd < 0)
|
||||||
|
return -1;
|
||||||
|
struct sockaddr_un address;
|
||||||
|
socklen_t length = daemon_address(&address, owner);
|
||||||
|
if (connect(fd, (struct sockaddr *)&address, length) == 0)
|
||||||
|
return fd;
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int daemon_main(pid_t owner)
|
||||||
|
{
|
||||||
|
if (setsid() < 0)
|
||||||
|
return 10;
|
||||||
|
prepare_webkit();
|
||||||
|
if (!gtk_init_check(NULL, NULL))
|
||||||
|
return 5;
|
||||||
|
|
||||||
|
int server_fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||||
|
if (server_fd < 0)
|
||||||
|
return 10;
|
||||||
|
struct sockaddr_un address;
|
||||||
|
socklen_t address_length = daemon_address(&address, owner);
|
||||||
|
if (bind(server_fd, (struct sockaddr *)&address, address_length) != 0 ||
|
||||||
|
listen(server_fd, 32) != 0) {
|
||||||
|
close(server_fd);
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *data_directory = g_build_filename(g_get_user_data_dir(),
|
||||||
|
"obs-webkit-browser", "profile", NULL);
|
||||||
|
char *cache_directory = g_build_filename(g_get_user_cache_dir(),
|
||||||
|
"obs-webkit-browser", "profile", NULL);
|
||||||
|
g_mkdir_with_parents(data_directory, 0700);
|
||||||
|
g_mkdir_with_parents(cache_directory, 0700);
|
||||||
|
WebKitWebsiteDataManager *data_manager = webkit_website_data_manager_new(
|
||||||
|
"base-data-directory", data_directory,
|
||||||
|
"base-cache-directory", cache_directory, NULL);
|
||||||
|
webkit_website_data_manager_set_persistent_credential_storage_enabled(data_manager, TRUE);
|
||||||
|
WebKitCookieManager *cookie_manager =
|
||||||
|
webkit_website_data_manager_get_cookie_manager(data_manager);
|
||||||
|
char *cookie_file = g_build_filename(data_directory, "cookies.sqlite", NULL);
|
||||||
|
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file,
|
||||||
|
WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
|
||||||
|
webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
|
||||||
|
shared_context = webkit_web_context_new_with_website_data_manager(data_manager);
|
||||||
|
webkit_web_context_set_cache_model(shared_context, WEBKIT_CACHE_MODEL_WEB_BROWSER);
|
||||||
|
g_object_unref(data_manager);
|
||||||
|
g_free(cookie_file);
|
||||||
|
g_free(data_directory);
|
||||||
|
g_free(cache_directory);
|
||||||
|
|
||||||
|
/* The first proxy inherited these descriptors when it forked this daemon.
|
||||||
|
* Only the SCM_RIGHTS copies accepted above may own an individual view. */
|
||||||
|
close(STDIN_FILENO);
|
||||||
|
close(STDOUT_FILENO);
|
||||||
|
close(STDERR_FILENO);
|
||||||
|
close(OWB_SHARED_FRAME_FD);
|
||||||
|
|
||||||
|
g_unix_fd_add(server_fd, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
|
||||||
|
accept_client, NULL);
|
||||||
|
g_unix_signal_add(SIGTERM, terminate_daemon, NULL);
|
||||||
|
g_unix_signal_add(SIGINT, terminate_daemon, NULL);
|
||||||
gtk_main();
|
gtk_main();
|
||||||
|
|
||||||
atomic_store_explicit(&renderer.stopping, true, memory_order_release);
|
while (renderers)
|
||||||
if (renderer.capture_thread_started)
|
destroy_renderer(renderers->data);
|
||||||
pthread_join(renderer.capture_thread, NULL);
|
if (idle_source) {
|
||||||
destroy_x11_capture(&renderer);
|
g_source_remove(idle_source);
|
||||||
munmap(renderer.shared, renderer.shared_size);
|
idle_source = 0;
|
||||||
free(renderer.config.url);
|
}
|
||||||
free(renderer.config.css);
|
g_object_unref(shared_context);
|
||||||
|
shared_context = NULL;
|
||||||
|
close(server_fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool pass_view_descriptors(int client_fd)
|
||||||
|
{
|
||||||
|
int descriptors[4] = {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO,
|
||||||
|
OWB_SHARED_FRAME_FD};
|
||||||
|
char byte = 1;
|
||||||
|
struct iovec payload = {.iov_base = &byte, .iov_len = sizeof(byte)};
|
||||||
|
char control[CMSG_SPACE(sizeof(descriptors))];
|
||||||
|
memset(control, 0, sizeof(control));
|
||||||
|
struct msghdr message = {0};
|
||||||
|
message.msg_iov = &payload;
|
||||||
|
message.msg_iovlen = 1;
|
||||||
|
message.msg_control = control;
|
||||||
|
message.msg_controllen = sizeof(control);
|
||||||
|
struct cmsghdr *header = CMSG_FIRSTHDR(&message);
|
||||||
|
header->cmsg_level = SOL_SOCKET;
|
||||||
|
header->cmsg_type = SCM_RIGHTS;
|
||||||
|
header->cmsg_len = CMSG_LEN(sizeof(descriptors));
|
||||||
|
memcpy(CMSG_DATA(header), descriptors, sizeof(descriptors));
|
||||||
|
return sendmsg(client_fd, &message, MSG_NOSIGNAL) == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
pid_t owner = getppid();
|
||||||
|
pid_t launched_daemon = 0;
|
||||||
|
int client_fd = open_daemon_socket(owner);
|
||||||
|
if (client_fd < 0) {
|
||||||
|
launched_daemon = fork();
|
||||||
|
if (launched_daemon < 0)
|
||||||
|
return 10;
|
||||||
|
if (launched_daemon == 0)
|
||||||
|
_exit(daemon_main(owner));
|
||||||
|
for (unsigned attempt = 0; attempt < 200 && client_fd < 0; attempt++) {
|
||||||
|
struct timespec pause = {.tv_nsec = 10000000L};
|
||||||
|
nanosleep(&pause, NULL);
|
||||||
|
client_fd = open_daemon_socket(owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (client_fd < 0) {
|
||||||
|
int status = 0;
|
||||||
|
if (launched_daemon > 0 && waitpid(launched_daemon, &status, WNOHANG) == launched_daemon &&
|
||||||
|
WIFEXITED(status))
|
||||||
|
return WEXITSTATUS(status);
|
||||||
|
fprintf(stderr, "Cannot start the shared WebKit renderer process\n");
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
if (launched_daemon > 0)
|
||||||
|
waitpid(launched_daemon, NULL, WNOHANG);
|
||||||
|
if (!pass_view_descriptors(client_fd)) {
|
||||||
|
fprintf(stderr, "Cannot attach view to the shared WebKit renderer process\n");
|
||||||
|
close(client_fd);
|
||||||
|
return 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(STDIN_FILENO);
|
||||||
|
close(STDOUT_FILENO);
|
||||||
|
close(STDERR_FILENO);
|
||||||
|
close(OWB_SHARED_FRAME_FD);
|
||||||
|
char ignored;
|
||||||
|
while (recv(client_fd, &ignored, sizeof(ignored), 0) < 0 && errno == EINTR) {
|
||||||
|
}
|
||||||
|
close(client_fd);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,211 @@
|
|||||||
#include <obs/obs.h>
|
#include <obs/obs.h>
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
struct audio_probe {
|
||||||
|
atomic_uint blocks;
|
||||||
|
atomic_bool signal;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool source_alpha_is_correct(obs_source_t *source)
|
||||||
|
{
|
||||||
|
bool correct = false;
|
||||||
|
uint8_t solid[4] = {0};
|
||||||
|
uint8_t half[4] = {0};
|
||||||
|
uint8_t background[4] = {0};
|
||||||
|
obs_set_output_source(0, source);
|
||||||
|
for (unsigned attempt = 0; attempt < 60 && !correct; attempt++) {
|
||||||
|
usleep(100000);
|
||||||
|
obs_enter_graphics();
|
||||||
|
gs_texrender_t *target = gs_texrender_create(GS_BGRA, GS_ZS_NONE);
|
||||||
|
gs_stagesurf_t *stage = gs_stagesurface_create(64, 64, GS_BGRA);
|
||||||
|
if (target && stage && gs_texrender_begin(target, 64, 64)) {
|
||||||
|
struct vec4 clear = {0};
|
||||||
|
gs_clear(GS_CLEAR_COLOR, &clear, 0.0f, 0);
|
||||||
|
gs_ortho(0.0f, 64.0f, 0.0f, 64.0f, -100.0f, 100.0f);
|
||||||
|
bool previous_srgb = gs_set_linear_srgb(true);
|
||||||
|
obs_source_video_render(source);
|
||||||
|
gs_set_linear_srgb(previous_srgb);
|
||||||
|
gs_texrender_end(target);
|
||||||
|
gs_stage_texture(stage, gs_texrender_get_texture(target));
|
||||||
|
uint8_t *pixels = NULL;
|
||||||
|
uint32_t stride = 0;
|
||||||
|
if (gs_stagesurface_map(stage, &pixels, &stride)) {
|
||||||
|
memcpy(background, pixels, sizeof(background));
|
||||||
|
memcpy(solid, pixels + (size_t)24 * stride + 24 * 4, sizeof(solid));
|
||||||
|
memcpy(half, pixels + (size_t)40 * stride + 40 * 4, sizeof(half));
|
||||||
|
gs_stagesurface_unmap(stage);
|
||||||
|
correct = solid[0] < 32 && solid[1] < 40 && solid[2] > 225 &&
|
||||||
|
solid[3] == 255 && half[0] < 8 && half[1] < 8 &&
|
||||||
|
half[2] > 120 && half[2] < 136 && half[3] > 120 &&
|
||||||
|
half[3] < 136 && background[0] == 0 &&
|
||||||
|
background[1] == 0 && background[2] == 0 &&
|
||||||
|
background[3] == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gs_stagesurface_destroy(stage);
|
||||||
|
gs_texrender_destroy(target);
|
||||||
|
obs_leave_graphics();
|
||||||
|
}
|
||||||
|
obs_set_output_source(0, NULL);
|
||||||
|
if (!correct)
|
||||||
|
fprintf(stderr,
|
||||||
|
"OBS compositing changed browser alpha (solid BGRA=%u,%u,%u,%u; "
|
||||||
|
"half BGRA=%u,%u,%u,%u; background BGRA=%u,%u,%u,%u)\n",
|
||||||
|
solid[0], solid[1], solid[2], solid[3], half[0], half[1], half[2],
|
||||||
|
half[3], background[0], background[1], background[2], background[3]);
|
||||||
|
return correct;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool write_all(int fd, const void *data, size_t size)
|
||||||
|
{
|
||||||
|
const uint8_t *cursor = data;
|
||||||
|
while (size) {
|
||||||
|
ssize_t written = write(fd, cursor, size);
|
||||||
|
if (written < 0 && errno == EINTR)
|
||||||
|
continue;
|
||||||
|
if (written <= 0)
|
||||||
|
return false;
|
||||||
|
cursor += (size_t)written;
|
||||||
|
size -= (size_t)written;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_u32_le(uint8_t *destination, uint32_t value)
|
||||||
|
{
|
||||||
|
for (unsigned index = 0; index < 4; index++)
|
||||||
|
destination[index] = (uint8_t)(value >> (index * 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_u16_le(uint8_t *destination, uint16_t value)
|
||||||
|
{
|
||||||
|
destination[0] = (uint8_t)value;
|
||||||
|
destination[1] = (uint8_t)(value >> 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void serve_audio(int server)
|
||||||
|
{
|
||||||
|
static const char page[] =
|
||||||
|
"<!doctype html><style>html,body{margin:0;width:100%;height:100%;background:#123}</style>"
|
||||||
|
"<audio id='tone' autoplay loop src='/tone.wav'></audio>"
|
||||||
|
"<script>tone.play().catch(e=>console.error('audio play failed',e))</script>";
|
||||||
|
const uint32_t frames = 48000;
|
||||||
|
const uint32_t data_size = frames * 2 * sizeof(int16_t);
|
||||||
|
uint8_t wav_header[44] = {0};
|
||||||
|
memcpy(wav_header, "RIFF", 4);
|
||||||
|
write_u32_le(wav_header + 4, 36 + data_size);
|
||||||
|
memcpy(wav_header + 8, "WAVEfmt ", 8);
|
||||||
|
write_u32_le(wav_header + 16, 16);
|
||||||
|
write_u16_le(wav_header + 20, 1);
|
||||||
|
write_u16_le(wav_header + 22, 2);
|
||||||
|
write_u32_le(wav_header + 24, 48000);
|
||||||
|
write_u32_le(wav_header + 28, 48000 * 2 * sizeof(int16_t));
|
||||||
|
write_u16_le(wav_header + 32, 2 * sizeof(int16_t));
|
||||||
|
write_u16_le(wav_header + 34, 16);
|
||||||
|
memcpy(wav_header + 36, "data", 4);
|
||||||
|
write_u32_le(wav_header + 40, data_size);
|
||||||
|
int16_t *samples = malloc(data_size);
|
||||||
|
if (!samples)
|
||||||
|
_exit(2);
|
||||||
|
for (uint32_t frame = 0; frame < frames; frame++) {
|
||||||
|
int16_t value = (frame / 55) % 2 ? 10000 : -10000;
|
||||||
|
samples[frame * 2] = value;
|
||||||
|
samples[frame * 2 + 1] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int client = accept(server, NULL, NULL);
|
||||||
|
if (client < 0 && errno == EINTR)
|
||||||
|
continue;
|
||||||
|
if (client < 0)
|
||||||
|
_exit(0);
|
||||||
|
char request[2048] = {0};
|
||||||
|
ssize_t count = read(client, request, sizeof(request) - 1);
|
||||||
|
bool wav = count > 0 && strstr(request, "GET /tone.wav ");
|
||||||
|
char response[512];
|
||||||
|
size_t body_size = wav ? sizeof(wav_header) + data_size : sizeof(page) - 1;
|
||||||
|
int length = snprintf(response, sizeof(response),
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nCache-Control: no-store\r\n"
|
||||||
|
"Connection: close\r\nContent-Length: %zu\r\n\r\n",
|
||||||
|
wav ? "audio/wav" : "text/html", body_size);
|
||||||
|
write_all(client, response, (size_t)length);
|
||||||
|
if (wav) {
|
||||||
|
write_all(client, wav_header, sizeof(wav_header));
|
||||||
|
write_all(client, samples, data_size);
|
||||||
|
} else {
|
||||||
|
write_all(client, page, sizeof(page) - 1);
|
||||||
|
}
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static pid_t start_audio_server(char *url, size_t url_size)
|
||||||
|
{
|
||||||
|
int server = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (server < 0)
|
||||||
|
return -1;
|
||||||
|
struct sockaddr_in address = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
|
||||||
|
if (bind(server, (struct sockaddr *)&address, sizeof(address)) != 0 || listen(server, 8) != 0) {
|
||||||
|
close(server);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
socklen_t size = sizeof(address);
|
||||||
|
if (getsockname(server, (struct sockaddr *)&address, &size) != 0) {
|
||||||
|
close(server);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
pid_t child = fork();
|
||||||
|
if (child == 0)
|
||||||
|
serve_audio(server);
|
||||||
|
close(server);
|
||||||
|
if (child < 0)
|
||||||
|
return -1;
|
||||||
|
snprintf(url, url_size, "http://127.0.0.1:%u/", ntohs(address.sin_port));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void audio_captured(void *opaque, obs_source_t *source,
|
||||||
|
const struct audio_data *audio, bool muted)
|
||||||
|
{
|
||||||
|
(void)source;
|
||||||
|
(void)muted;
|
||||||
|
struct audio_probe *probe = opaque;
|
||||||
|
atomic_fetch_add(&probe->blocks, 1);
|
||||||
|
if (!audio->data[0])
|
||||||
|
return;
|
||||||
|
const float *samples = (const float *)audio->data[0];
|
||||||
|
for (uint32_t index = 0; index < audio->frames; index++) {
|
||||||
|
float value = samples[index];
|
||||||
|
if (value > 0.01f || value < -0.01f) {
|
||||||
|
atomic_store(&probe->signal, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
if (argc != 4) {
|
if (argc < 4 || argc > 5) {
|
||||||
fprintf(stderr, "usage: %s plugin.so data-directory renderer\n", argv[0]);
|
fprintf(stderr,
|
||||||
|
"usage: %s plugin.so data-directory renderer [audio-test | alpha-test]\n",
|
||||||
|
argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
bool audio_test = argc == 5 && strcmp(argv[4], "audio-test") == 0;
|
||||||
|
bool alpha_test = argc == 5 && strcmp(argv[4], "alpha-test") == 0;
|
||||||
char temporary[] = "/tmp/obs-webkit-module-test-XXXXXX";
|
char temporary[] = "/tmp/obs-webkit-module-test-XXXXXX";
|
||||||
if (!mkdtemp(temporary)) {
|
if (!mkdtemp(temporary)) {
|
||||||
perror("mkdtemp");
|
perror("mkdtemp");
|
||||||
@@ -34,25 +228,74 @@ int main(int argc, char **argv)
|
|||||||
rmdir(temporary);
|
rmdir(temporary);
|
||||||
return 77;
|
return 77;
|
||||||
}
|
}
|
||||||
|
struct obs_audio_info audio_info = {.samples_per_sec = 48000, .speakers = SPEAKERS_STEREO};
|
||||||
|
if (!obs_reset_audio(&audio_info)) {
|
||||||
|
obs_shutdown();
|
||||||
|
return 77;
|
||||||
|
}
|
||||||
|
if (alpha_test) {
|
||||||
|
struct obs_video_info video_info = {
|
||||||
|
.graphics_module = "libobs-opengl",
|
||||||
|
.fps_num = 30,
|
||||||
|
.fps_den = 1,
|
||||||
|
.base_width = 64,
|
||||||
|
.base_height = 64,
|
||||||
|
.output_width = 64,
|
||||||
|
.output_height = 64,
|
||||||
|
.output_format = VIDEO_FORMAT_BGRA,
|
||||||
|
.gpu_conversion = false,
|
||||||
|
.colorspace = VIDEO_CS_DEFAULT,
|
||||||
|
.range = VIDEO_RANGE_FULL,
|
||||||
|
.scale_type = OBS_SCALE_DISABLE,
|
||||||
|
};
|
||||||
|
if (obs_reset_video(&video_info) != OBS_VIDEO_SUCCESS) {
|
||||||
|
obs_shutdown();
|
||||||
|
return 77;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char audio_url[128] = {0};
|
||||||
|
pid_t audio_server = audio_test ? start_audio_server(audio_url, sizeof(audio_url)) : -1;
|
||||||
|
if (audio_test && audio_server < 0) {
|
||||||
|
obs_shutdown();
|
||||||
|
return 77;
|
||||||
|
}
|
||||||
obs_module_t *module = NULL;
|
obs_module_t *module = NULL;
|
||||||
int result = obs_open_module(&module, argv[1], temporary);
|
int result = obs_open_module(&module, argv[1], temporary);
|
||||||
if (result != MODULE_SUCCESS || !obs_init_module(module)) {
|
if (result != MODULE_SUCCESS || !obs_init_module(module)) {
|
||||||
fprintf(stderr, "module load failed: %d\n", result);
|
fprintf(stderr, "module load failed: %d\n", result);
|
||||||
|
if (audio_server > 0) {
|
||||||
|
kill(audio_server, SIGTERM);
|
||||||
|
waitpid(audio_server, NULL, 0);
|
||||||
|
}
|
||||||
obs_shutdown();
|
obs_shutdown();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const char *name = obs_source_get_display_name("webkit_browser_source");
|
const char *name = obs_source_get_display_name("webkit_browser_source");
|
||||||
uint32_t flags = obs_get_source_output_flags("webkit_browser_source");
|
uint32_t flags = obs_get_source_output_flags("webkit_browser_source");
|
||||||
bool valid = name && strstr(name, "WebKit") && (flags & OBS_SOURCE_ASYNC_VIDEO) &&
|
bool valid = name && strstr(name, "WebKit") && (flags & OBS_SOURCE_ASYNC_VIDEO) &&
|
||||||
(flags & OBS_SOURCE_INTERACTION);
|
(flags & OBS_SOURCE_AUDIO) && (flags & OBS_SOURCE_INTERACTION) &&
|
||||||
|
(flags & OBS_SOURCE_SRGB);
|
||||||
if (!valid)
|
if (!valid)
|
||||||
fprintf(stderr, "source registration is incomplete (name=%s, flags=0x%x)\n",
|
fprintf(stderr, "source registration is incomplete (name=%s, flags=0x%x)\n",
|
||||||
name ? name : "(null)", flags);
|
name ? name : "(null)", flags);
|
||||||
obs_data_t *settings = obs_data_create();
|
obs_data_t *settings = obs_data_create();
|
||||||
obs_data_set_string(settings, "url", "data:text/html,<body style='background:red'></body>");
|
const char *test_url = "data:text/html,<body style='background:red'></body>";
|
||||||
|
if (audio_test)
|
||||||
|
test_url = audio_url;
|
||||||
|
else if (alpha_test)
|
||||||
|
test_url = "data:text/html,<body><div id='box'></div></body>";
|
||||||
|
obs_data_set_string(settings, "url", test_url);
|
||||||
|
if (alpha_test)
|
||||||
|
obs_data_set_string(settings, "css",
|
||||||
|
"html,body{margin:0}#box{position:fixed;left:16px;top:16px;"
|
||||||
|
"width:16px;height:16px;background:rgb(241,23,17)}"
|
||||||
|
"#box:after{content:'';position:fixed;left:32px;top:32px;"
|
||||||
|
"width:16px;height:16px;background:rgba(255,0,0,.5)}");
|
||||||
obs_data_set_int(settings, "width", 64);
|
obs_data_set_int(settings, "width", 64);
|
||||||
obs_data_set_int(settings, "height", 64);
|
obs_data_set_int(settings, "height", 64);
|
||||||
obs_data_set_int(settings, "fps", 10);
|
obs_data_set_int(settings, "fps", 10);
|
||||||
|
obs_data_set_int(settings, "zoom", alpha_test ? 100 : 175);
|
||||||
|
obs_data_set_bool(settings, "capture_audio", audio_test);
|
||||||
obs_data_set_bool(settings, "transparent", true);
|
obs_data_set_bool(settings, "transparent", true);
|
||||||
obs_data_set_bool(settings, "hardware_acceleration", false);
|
obs_data_set_bool(settings, "hardware_acceleration", false);
|
||||||
obs_source_t *source = obs_source_create_private("webkit_browser_source", "smoke", settings);
|
obs_source_t *source = obs_source_create_private("webkit_browser_source", "smoke", settings);
|
||||||
@@ -61,13 +304,53 @@ int main(int argc, char **argv)
|
|||||||
fprintf(stderr, "source lifecycle setup failed\n");
|
fprintf(stderr, "source lifecycle setup failed\n");
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
obs_properties_t *properties = source ? obs_source_properties(source) : NULL;
|
||||||
|
obs_property_t *zoom = properties ? obs_properties_get(properties, "zoom") : NULL;
|
||||||
|
obs_property_t *capture_audio_property =
|
||||||
|
properties ? obs_properties_get(properties, "capture_audio") : NULL;
|
||||||
|
if (!zoom || obs_property_get_type(zoom) != OBS_PROPERTY_INT ||
|
||||||
|
obs_property_int_type(zoom) != OBS_NUMBER_SLIDER || obs_property_int_min(zoom) != 25 ||
|
||||||
|
obs_property_int_max(zoom) != 500 || obs_property_int_step(zoom) != 5) {
|
||||||
|
fprintf(stderr, "page zoom property is missing or invalid\n");
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
if (!capture_audio_property || obs_property_get_type(capture_audio_property) != OBS_PROPERTY_BOOL ||
|
||||||
|
!source ||
|
||||||
|
obs_source_audio_active(source) != audio_test) {
|
||||||
|
fprintf(stderr, "browser audio capture property or inactive mixer state is invalid\n");
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
if (properties)
|
||||||
|
obs_properties_destroy(properties);
|
||||||
|
struct audio_probe probe;
|
||||||
|
atomic_init(&probe.blocks, 0);
|
||||||
|
atomic_init(&probe.signal, false);
|
||||||
|
if (source && audio_test)
|
||||||
|
obs_source_add_audio_capture_callback(source, audio_captured, &probe);
|
||||||
if (source) {
|
if (source) {
|
||||||
usleep(500000);
|
if (alpha_test && !source_alpha_is_correct(source))
|
||||||
|
valid = false;
|
||||||
|
unsigned attempts = audio_test ? 100 : 5;
|
||||||
|
for (unsigned attempt = 0; attempt < attempts &&
|
||||||
|
(!audio_test || !atomic_load(&probe.signal)); attempt++)
|
||||||
|
usleep(100000);
|
||||||
|
if (audio_test) {
|
||||||
|
obs_source_remove_audio_capture_callback(source, audio_captured, &probe);
|
||||||
|
if (!atomic_load(&probe.signal)) {
|
||||||
|
fprintf(stderr, "browser audio did not reach the OBS source (blocks=%u)\n",
|
||||||
|
atomic_load(&probe.blocks));
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
obs_source_release(source);
|
obs_source_release(source);
|
||||||
}
|
}
|
||||||
obs_shutdown();
|
obs_shutdown();
|
||||||
unlink(renderer_link);
|
unlink(renderer_link);
|
||||||
unlink(locale_link);
|
unlink(locale_link);
|
||||||
rmdir(temporary);
|
rmdir(temporary);
|
||||||
|
if (audio_server > 0) {
|
||||||
|
kill(audio_server, SIGTERM);
|
||||||
|
waitpid(audio_server, NULL, 0);
|
||||||
|
}
|
||||||
return valid ? 0 : 1;
|
return valid ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@
|
|||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
assert(sizeof(struct owb_config_header) == 36);
|
assert(sizeof(struct owb_config_header) == 108);
|
||||||
|
assert(offsetof(struct owb_config_header, zoom_percent) == 20);
|
||||||
|
assert(offsetof(struct owb_config_header, audio_sink) == 44);
|
||||||
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);
|
||||||
@@ -109,6 +109,7 @@ int main(int argc, char **argv)
|
|||||||
struct owb_config_header config = {
|
struct owb_config_header config = {
|
||||||
.magic = OWB_CONFIG_MAGIC, .version = OWB_PROTOCOL_VERSION,
|
.magic = OWB_CONFIG_MAGIC, .version = OWB_PROTOCOL_VERSION,
|
||||||
.width = width, .height = height, .fps = requested_fps, .transparent = 1,
|
.width = width, .height = height, .fps = requested_fps, .transparent = 1,
|
||||||
|
.zoom_percent = 100,
|
||||||
.hardware_acceleration = 1,
|
.hardware_acceleration = 1,
|
||||||
.url_length = sizeof(url) - 1, .css_length = (uint32_t)css_length,
|
.url_length = sizeof(url) - 1, .css_length = (uint32_t)css_length,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,12 +56,13 @@ int main(int argc, char **argv)
|
|||||||
{
|
{
|
||||||
if (argc < 2 || argc > 4) {
|
if (argc < 2 || argc > 4) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"usage: %s /path/to/obs-webkit-renderer [opaque | opaque-url URL]\n",
|
"usage: %s /path/to/obs-webkit-renderer [opaque | zoom | opaque-url URL]\n",
|
||||||
argv[0]);
|
argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
bool external_url = argc == 4 && strcmp(argv[2], "opaque-url") == 0;
|
bool external_url = argc == 4 && strcmp(argv[2], "opaque-url") == 0;
|
||||||
bool opaque = external_url || (argc == 3 && strcmp(argv[2], "opaque") == 0);
|
bool zoom = argc == 3 && strcmp(argv[2], "zoom") == 0;
|
||||||
|
bool opaque = external_url || zoom || (argc == 3 && strcmp(argv[2], "opaque") == 0);
|
||||||
int control[2];
|
int control[2];
|
||||||
int frames[2];
|
int frames[2];
|
||||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) || pipe(frames)) {
|
if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) || pipe(frames)) {
|
||||||
@@ -84,7 +85,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);
|
||||||
@@ -103,30 +104,43 @@ int main(int argc, char **argv)
|
|||||||
|
|
||||||
const char internal_url[] =
|
const char internal_url[] =
|
||||||
"data:text/html,<body style='background:rgb(3,5,7)'><div id='box'></div></body>";
|
"data:text/html,<body style='background:rgb(3,5,7)'><div id='box'></div></body>";
|
||||||
const char transparent_css[] = "html,body{margin:0}"
|
const char transparent_css[] =
|
||||||
"#box{position:fixed;inset:16px;background:rgba(255,0,0,.5)}";
|
"html,body{margin:0}"
|
||||||
|
"#box{position:fixed;left:16px;top:16px;width:16px;height:16px;"
|
||||||
|
"background:rgb(241,23,17)}"
|
||||||
|
"#box:after{content:'';position:fixed;left:32px;top:32px;width:16px;"
|
||||||
|
"height:16px;background:rgba(255,0,0,.5)}";
|
||||||
const char opaque_css[] = "html,body{margin:0;background:rgb(7,19,31)!important}"
|
const char opaque_css[] = "html,body{margin:0;background:rgb(7,19,31)!important}"
|
||||||
"#box{position:fixed;left:16px;top:16px;width:32px;height:32px;"
|
"#box{position:fixed;left:16px;top:16px;width:32px;height:32px;"
|
||||||
"background:rgb(241,23,17);transform:translateZ(0);filter:blur(.1px)}";
|
"background:rgb(241,23,17);transform:translateZ(0);filter:blur(.1px)}";
|
||||||
|
const char zoom_css[] = "html,body{margin:0;background:rgb(7,19,31)!important}"
|
||||||
|
"#box{position:fixed;left:0;top:0;width:10px;height:10px;"
|
||||||
|
"background:rgb(241,23,17)}";
|
||||||
const char *url = external_url ? argv[3] : internal_url;
|
const char *url = external_url ? argv[3] : internal_url;
|
||||||
const char *css = external_url ? "" : opaque ? opaque_css : transparent_css;
|
const char *css = external_url ? "" : zoom ? zoom_css : opaque ? opaque_css : transparent_css;
|
||||||
|
const char *audio_sink = getenv("OWB_TEST_AUDIO_SINK");
|
||||||
struct owb_config_header config = {
|
struct owb_config_header config = {
|
||||||
.magic = OWB_CONFIG_MAGIC,
|
.magic = OWB_CONFIG_MAGIC,
|
||||||
.version = OWB_PROTOCOL_VERSION,
|
.version = OWB_PROTOCOL_VERSION,
|
||||||
.width = width,
|
.width = width,
|
||||||
.height = height,
|
.height = height,
|
||||||
.fps = 10,
|
.fps = 10,
|
||||||
|
.zoom_percent = zoom ? 200 : 100,
|
||||||
|
.capture_audio = audio_sink && *audio_sink,
|
||||||
.transparent = opaque ? 0 : 1,
|
.transparent = opaque ? 0 : 1,
|
||||||
/* GPU rendering must still produce an invisible captured frame. */
|
/* GPU rendering must still produce an invisible captured frame. */
|
||||||
.hardware_acceleration = 1,
|
.hardware_acceleration = 1,
|
||||||
.url_length = (uint32_t)strlen(url),
|
.url_length = (uint32_t)strlen(url),
|
||||||
.css_length = (uint32_t)strlen(css),
|
.css_length = (uint32_t)strlen(css),
|
||||||
};
|
};
|
||||||
|
if (audio_sink)
|
||||||
|
snprintf(config.audio_sink, sizeof(config.audio_sink), "%s", audio_sink);
|
||||||
bool sent = write_exact(control[0], &config, sizeof(config)) &&
|
bool sent = write_exact(control[0], &config, sizeof(config)) &&
|
||||||
write_exact(control[0], url, strlen(url)) &&
|
write_exact(control[0], url, strlen(url)) &&
|
||||||
write_exact(control[0], css, strlen(css));
|
write_exact(control[0], css, strlen(css));
|
||||||
uint8_t last_bgra[4] = {0};
|
uint8_t last_bgra[4] = {0};
|
||||||
uint8_t last_corner_bgra[4] = {0};
|
uint8_t last_corner_bgra[4] = {0};
|
||||||
|
uint8_t last_half_bgra[4] = {0};
|
||||||
bool found_red = false;
|
bool found_red = false;
|
||||||
int max_attempts = external_url ? 120 : 60;
|
int max_attempts = external_url ? 120 : 60;
|
||||||
for (int attempt = 0; sent && attempt < max_attempts; attempt++) {
|
for (int attempt = 0; sent && attempt < max_attempts; attempt++) {
|
||||||
@@ -158,14 +172,28 @@ int main(int argc, char **argv)
|
|||||||
nonuniform ? "content" : "uniform");
|
nonuniform ? "content" : "uniform");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
size_t center = ((size_t)(height / 2) * width + width / 2) * 4;
|
size_t center = zoom ? ((size_t)5 * width + 15) * 4
|
||||||
|
: opaque ? ((size_t)(height / 2) * width + width / 2) * 4
|
||||||
|
: ((size_t)24 * width + 24) * 4;
|
||||||
memcpy(last_bgra, frame + center, sizeof(last_bgra));
|
memcpy(last_bgra, frame + center, sizeof(last_bgra));
|
||||||
memcpy(last_corner_bgra, frame, sizeof(last_corner_bgra));
|
size_t corner = zoom ? ((size_t)5 * width + 25) * 4 : 0;
|
||||||
bool expected = opaque
|
memcpy(last_corner_bgra, frame + corner, sizeof(last_corner_bgra));
|
||||||
|
if (!zoom && !opaque) {
|
||||||
|
size_t half = ((size_t)40 * width + 40) * 4;
|
||||||
|
memcpy(last_half_bgra, frame + half, sizeof(last_half_bgra));
|
||||||
|
}
|
||||||
|
bool expected = zoom
|
||||||
|
? last_bgra[0] < 32 && last_bgra[1] < 40 && last_bgra[2] > 210 &&
|
||||||
|
last_bgra[3] == 255 && last_corner_bgra[0] > 20 &&
|
||||||
|
last_corner_bgra[1] < 32 && last_corner_bgra[2] < 32 &&
|
||||||
|
last_corner_bgra[3] == 255
|
||||||
|
: opaque
|
||||||
? last_bgra[0] < 32 && last_bgra[1] < 40 && last_bgra[2] > 210 &&
|
? last_bgra[0] < 32 && last_bgra[1] < 40 && last_bgra[2] > 210 &&
|
||||||
last_bgra[3] == 255
|
last_bgra[3] == 255
|
||||||
: last_bgra[0] < 8 && last_bgra[1] < 8 && last_bgra[2] > 248 &&
|
: last_bgra[0] < 32 && last_bgra[1] < 40 && last_bgra[2] > 225 &&
|
||||||
last_bgra[3] > 120 && last_bgra[3] < 136 &&
|
last_bgra[3] == 255 && last_half_bgra[0] < 8 &&
|
||||||
|
last_half_bgra[1] < 8 && last_half_bgra[2] > 248 &&
|
||||||
|
last_half_bgra[3] > 120 && last_half_bgra[3] < 136 &&
|
||||||
last_corner_bgra[0] == 0 && last_corner_bgra[1] == 0 &&
|
last_corner_bgra[0] == 0 && last_corner_bgra[1] == 0 &&
|
||||||
last_corner_bgra[2] == 0 && last_corner_bgra[3] == 0;
|
last_corner_bgra[2] == 0 && last_corner_bgra[3] == 0;
|
||||||
if (expected) {
|
if (expected) {
|
||||||
@@ -191,9 +219,12 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"renderer did not produce the expected %s red BGRA frame "
|
"renderer did not produce the expected %s red BGRA frame "
|
||||||
"(center BGRA=%u,%u,%u,%u; corner BGRA=%u,%u,%u,%u)\n",
|
"(center BGRA=%u,%u,%u,%u; half BGRA=%u,%u,%u,%u; "
|
||||||
opaque ? "opaque" : "transparent", last_bgra[0], last_bgra[1],
|
"corner BGRA=%u,%u,%u,%u)\n",
|
||||||
last_bgra[2], last_bgra[3], last_corner_bgra[0], last_corner_bgra[1],
|
zoom ? "200%-zoomed" : opaque ? "opaque" : "transparent",
|
||||||
|
last_bgra[0], last_bgra[1],
|
||||||
|
last_bgra[2], last_bgra[3], last_half_bgra[0], last_half_bgra[1],
|
||||||
|
last_half_bgra[2], last_half_bgra[3], last_corner_bgra[0], last_corner_bgra[1],
|
||||||
last_corner_bgra[2], last_corner_bgra[3]);
|
last_corner_bgra[2], last_corner_bgra[3]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
203
tests/shared-profile-test.c
Normal file
203
tests/shared-profile-test.c
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
#include "protocol.h"
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <spawn.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
extern char **environ;
|
||||||
|
|
||||||
|
struct view {
|
||||||
|
pid_t pid;
|
||||||
|
int control;
|
||||||
|
int frames;
|
||||||
|
int shared_fd;
|
||||||
|
struct owb_shared_frames *shared;
|
||||||
|
size_t shared_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool write_all(int fd, const void *data, size_t size)
|
||||||
|
{
|
||||||
|
const uint8_t *cursor = data;
|
||||||
|
while (size) {
|
||||||
|
ssize_t count = write(fd, cursor, size);
|
||||||
|
if (count < 0 && errno == EINTR)
|
||||||
|
continue;
|
||||||
|
if (count <= 0)
|
||||||
|
return false;
|
||||||
|
cursor += (size_t)count;
|
||||||
|
size -= (size_t)count;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void serve_pages(int server)
|
||||||
|
{
|
||||||
|
static const char page[] =
|
||||||
|
"<!doctype html><style>html,body{margin:0;width:100%;height:100%;background:rgb(2,3,5)}</style>"
|
||||||
|
"<script>const p=new URL(location.href),t=p.searchParams.get('token'),"
|
||||||
|
"k='owb-shared-profile-test',b=new BroadcastChannel(k);"
|
||||||
|
"if(p.pathname==='/writer'){setInterval(()=>{localStorage.setItem(k,t);b.postMessage(t)},100)}"
|
||||||
|
"else{let heard=false;b.onmessage=e=>{if(e.data===t)heard=true};"
|
||||||
|
"setInterval(()=>{if(heard&&localStorage.getItem(k)===t)"
|
||||||
|
"document.body.style.background='rgb(17,231,43)'},50)}</script>";
|
||||||
|
char response[2048];
|
||||||
|
int length = snprintf(response, sizeof(response),
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nCache-Control: no-store\r\n"
|
||||||
|
"Connection: close\r\nContent-Length: %zu\r\n\r\n%s",
|
||||||
|
strlen(page), page);
|
||||||
|
for (;;) {
|
||||||
|
int client = accept(server, NULL, NULL);
|
||||||
|
if (client < 0 && errno == EINTR)
|
||||||
|
continue;
|
||||||
|
if (client < 0)
|
||||||
|
_exit(0);
|
||||||
|
char request[1024];
|
||||||
|
ssize_t ignored = read(client, request, sizeof(request));
|
||||||
|
(void)ignored;
|
||||||
|
write_all(client, response, (size_t)length);
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool start_view(struct view *view, const char *renderer, const char *url)
|
||||||
|
{
|
||||||
|
memset(view, 0, sizeof(*view));
|
||||||
|
view->pid = 0;
|
||||||
|
view->control = view->frames = view->shared_fd = -1;
|
||||||
|
int control[2] = {-1, -1};
|
||||||
|
int frames[2] = {-1, -1};
|
||||||
|
if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) != 0 || pipe(frames) != 0)
|
||||||
|
return false;
|
||||||
|
const uint32_t width = 64, height = 64;
|
||||||
|
view->shared_size = owb_shared_size(width, height);
|
||||||
|
view->shared_fd = memfd_create("shared-profile-test", MFD_CLOEXEC);
|
||||||
|
if (view->shared_fd < 0 || ftruncate(view->shared_fd, (off_t)view->shared_size) != 0)
|
||||||
|
return false;
|
||||||
|
view->shared = mmap(NULL, view->shared_size, PROT_READ | PROT_WRITE, MAP_SHARED,
|
||||||
|
view->shared_fd, 0);
|
||||||
|
if (view->shared == MAP_FAILED)
|
||||||
|
return false;
|
||||||
|
memset(view->shared, 0, sizeof(*view->shared));
|
||||||
|
view->shared->magic = OWB_SHARED_MAGIC;
|
||||||
|
view->shared->version = OWB_PROTOCOL_VERSION;
|
||||||
|
view->shared->width = width;
|
||||||
|
view->shared->height = height;
|
||||||
|
view->shared->stride = width * 4;
|
||||||
|
view->shared->slot_count = OWB_SHARED_FRAME_SLOTS;
|
||||||
|
|
||||||
|
posix_spawn_file_actions_t actions;
|
||||||
|
posix_spawn_file_actions_init(&actions);
|
||||||
|
posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO);
|
||||||
|
posix_spawn_file_actions_adddup2(&actions, frames[1], STDOUT_FILENO);
|
||||||
|
posix_spawn_file_actions_adddup2(&actions, view->shared_fd, OWB_SHARED_FRAME_FD);
|
||||||
|
char *arguments[] = {(char *)renderer, NULL};
|
||||||
|
int error = posix_spawn(&view->pid, renderer, &actions, NULL, arguments, environ);
|
||||||
|
posix_spawn_file_actions_destroy(&actions);
|
||||||
|
close(control[1]);
|
||||||
|
close(frames[1]);
|
||||||
|
if (error)
|
||||||
|
return false;
|
||||||
|
view->control = control[0];
|
||||||
|
view->frames = frames[0];
|
||||||
|
struct owb_config_header config = {
|
||||||
|
.magic = OWB_CONFIG_MAGIC, .version = OWB_PROTOCOL_VERSION,
|
||||||
|
.width = width, .height = height, .fps = 20, .transparent = 0,
|
||||||
|
.zoom_percent = 100,
|
||||||
|
.hardware_acceleration = 1, .url_length = (uint32_t)strlen(url), .css_length = 0,
|
||||||
|
};
|
||||||
|
return write_all(view->control, &config, sizeof(config)) &&
|
||||||
|
write_all(view->control, url, strlen(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int stop_view(struct view *view)
|
||||||
|
{
|
||||||
|
int status = 0;
|
||||||
|
if (view->control >= 0) {
|
||||||
|
shutdown(view->control, SHUT_RDWR);
|
||||||
|
close(view->control);
|
||||||
|
}
|
||||||
|
if (view->frames >= 0)
|
||||||
|
close(view->frames);
|
||||||
|
if (view->shared && view->shared != MAP_FAILED)
|
||||||
|
munmap(view->shared, view->shared_size);
|
||||||
|
if (view->shared_fd >= 0)
|
||||||
|
close(view->shared_fd);
|
||||||
|
if (view->pid > 0) {
|
||||||
|
kill(view->pid, SIGTERM);
|
||||||
|
while (waitpid(view->pid, &status, 0) < 0 && errno == EINTR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (argc != 2)
|
||||||
|
return 2;
|
||||||
|
int server = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
int enabled = 1;
|
||||||
|
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled));
|
||||||
|
struct sockaddr_in address = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
|
||||||
|
if (server < 0 || bind(server, (struct sockaddr *)&address, sizeof(address)) != 0 ||
|
||||||
|
listen(server, 16) != 0)
|
||||||
|
return 77;
|
||||||
|
socklen_t address_size = sizeof(address);
|
||||||
|
if (getsockname(server, (struct sockaddr *)&address, &address_size) != 0)
|
||||||
|
return 2;
|
||||||
|
pid_t http_server = fork();
|
||||||
|
if (http_server == 0)
|
||||||
|
serve_pages(server);
|
||||||
|
close(server);
|
||||||
|
|
||||||
|
char writer_url[256], reader_url[256];
|
||||||
|
unsigned port = ntohs(address.sin_port);
|
||||||
|
snprintf(writer_url, sizeof(writer_url), "http://127.0.0.1:%u/writer?token=%ld", port,
|
||||||
|
(long)getpid());
|
||||||
|
snprintf(reader_url, sizeof(reader_url), "http://127.0.0.1:%u/reader?token=%ld", port,
|
||||||
|
(long)getpid());
|
||||||
|
struct view writer, reader;
|
||||||
|
bool started_writer = start_view(&writer, argv[1], writer_url);
|
||||||
|
bool started_reader = started_writer && start_view(&reader, argv[1], reader_url);
|
||||||
|
bool shared = false;
|
||||||
|
for (unsigned attempt = 0; started_reader && attempt < 200; attempt++) {
|
||||||
|
struct pollfd item = {.fd = reader.frames, .events = POLLIN};
|
||||||
|
if (poll(&item, 1, 100) <= 0)
|
||||||
|
continue;
|
||||||
|
uint8_t slot;
|
||||||
|
if (read(reader.frames, &slot, sizeof(slot)) != 1 || slot >= OWB_SHARED_FRAME_SLOTS)
|
||||||
|
continue;
|
||||||
|
uint8_t *frame = owb_shared_slot(reader.shared, slot);
|
||||||
|
size_t center = ((size_t)32 * 64 + 32) * 4;
|
||||||
|
uint8_t *bgra = frame + center;
|
||||||
|
if (bgra[0] < 55 && bgra[1] > 210 && bgra[2] < 35 && bgra[3] == 255) {
|
||||||
|
shared = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int reader_status = started_reader ? stop_view(&reader) : 0;
|
||||||
|
int writer_status = started_writer ? stop_view(&writer) : 0;
|
||||||
|
kill(http_server, SIGTERM);
|
||||||
|
waitpid(http_server, NULL, 0);
|
||||||
|
if (!started_writer || !started_reader)
|
||||||
|
return 2;
|
||||||
|
if ((WIFEXITED(reader_status) && WEXITSTATUS(reader_status) == 5) ||
|
||||||
|
(WIFEXITED(writer_status) && WEXITSTATUS(writer_status) == 5))
|
||||||
|
return 77;
|
||||||
|
if (!shared) {
|
||||||
|
fprintf(stderr, "two renderer views did not share localStorage and BroadcastChannel\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user