diff --git a/CMakeLists.txt b/CMakeLists.txt index f9c8031..e26e6bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -project(obs-webkit-browser VERSION 1.1.0 LANGUAGES C CXX) +project(obs-webkit-browser VERSION 1.2.0 LANGUAGES C CXX) include(GNUInstallDirs) find_package(PkgConfig REQUIRED) @@ -79,6 +79,10 @@ if(BUILD_TESTING) COMMAND renderer-smoke-test $ opaque) 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 $ 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) target_compile_features(renderer-fps-test PRIVATE c_std_11) target_compile_definitions(renderer-fps-test PRIVATE _GNU_SOURCE) diff --git a/README.md b/README.md index ccfd7b6..16e85be 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ DMA-BUF. Кадры передаются в OBS напрямую как BGRA, б - HTTP/HTTPS, `data:` URL и локальные HTML-файлы; - прозрачный фон с alpha-каналом; - пользовательский CSS; +- масштаб страницы 25–500% с сохранением в настройках источника или web-панели; - размер 2–8192 px и 1–60 FPS; - аппаратный WebKit compositor через DRM/DMA-BUF (Mesa `radeonsi` на AMD); - прямой XComposite/XShm-захват скомпозированного GPU-кадра с alpha; diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index cbd2be9..4952fe5 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -13,6 +13,7 @@ URL="URL or local HTML file" Width="Width" Height="Height" FPS="Frames per second" +PageZoom="Page zoom (%)" Transparent="Transparent background" CustomCSS="Custom CSS" ShutdownWhenHidden="Shut down renderer when source is hidden" diff --git a/data/locale/ru-RU.ini b/data/locale/ru-RU.ini index e8ff4f1..5d96cbe 100644 --- a/data/locale/ru-RU.ini +++ b/data/locale/ru-RU.ini @@ -13,6 +13,7 @@ URL="URL или локальный HTML-файл" Width="Ширина" Height="Высота" FPS="Кадров в секунду" +PageZoom="Масштаб страницы (%)" Transparent="Прозрачный фон" CustomCSS="Пользовательский CSS" ShutdownWhenHidden="Останавливать рендерер, когда источник скрыт" diff --git a/src/docks.cpp b/src/docks.cpp index 63cd849..4712502 100644 --- a/src/docks.cpp +++ b/src/docks.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -108,11 +109,13 @@ struct DockDefinition { QString id; QString title; QString url; + int zoomPercent = 100; }; class WebPanel final : public QWidget { public: - explicit WebPanel(QString url, QWidget *parent = nullptr) : QWidget(parent), url_(std::move(url)) + explicit WebPanel(QString url, int zoomPercent, QWidget *parent = nullptr) + : QWidget(parent), url_(std::move(url)), zoomPercent_(zoomPercent) { setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); @@ -127,11 +130,13 @@ public: ~WebPanel() override { stopRenderer(); } - void setUrl(const QString &url) + void setConfiguration(const QString &url, int zoomPercent) { - if (url_ == url) + zoomPercent = std::clamp(zoomPercent, 25, 500); + if (url_ == url && zoomPercent_ == zoomPercent) return; url_ = url; + zoomPercent_ = zoomPercent; if (isVisible()) restartRenderer(); } @@ -343,6 +348,7 @@ private: header.width = static_cast(renderWidth_); header.height = static_cast(renderHeight_); header.fps = 30; + header.zoom_percent = static_cast(zoomPercent_); header.transparent = 0; header.hardware_acceleration = 1; header.url_length = static_cast(url.size()); @@ -569,6 +575,7 @@ private: } QString url_; + int zoomPercent_ = 100; QString status_; QImage frame_; QTimer resizeTimer_; @@ -732,6 +739,7 @@ private: 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); } @@ -750,7 +758,11 @@ private: 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(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()) @@ -780,11 +792,12 @@ private: } for (const DockDefinition &definition : definitions_) { if (panels_.contains(definition.id) && panels_[definition.id]) { - panels_[definition.id]->setUrl(definition.url); + panels_[definition.id]->setConfiguration(definition.url, + definition.zoomPercent); panels_[definition.id]->setDockTitle(definition.title); continue; } - auto *panel = new WebPanel(definition.url); + 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)) @@ -802,10 +815,11 @@ private: dialog.resize(720, 360); auto *layout = new QVBoxLayout(&dialog); layout->addWidget(new QLabel(text("CustomBrowserDocksDescription"), &dialog)); - auto *table = new QTableWidget(0, 2, &dialog); - table->setHorizontalHeaderLabels({text("PanelName"), text("URL")}); + 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); @@ -817,6 +831,12 @@ private: 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); @@ -829,7 +849,7 @@ private: rowButtons->addStretch(); layout->addLayout(rowButtons); QObject::connect(add, &QPushButton::clicked, table, [table, appendRow] { - appendRow({QUuid::createUuid().toString(QUuid::WithoutBraces), QString(), "https://"}); + appendRow({QUuid::createUuid().toString(QUuid::WithoutBraces), QString(), "https://", 100}); int row = table->rowCount() - 1; table->setCurrentCell(row, 0); table->editItem(table->item(row, 0)); @@ -856,6 +876,7 @@ private: 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(table->cellWidget(row, 2)); if (title.isEmpty() || url.isEmpty()) { QMessageBox::warning(&dialog, text("CustomBrowserDocks"), text("PanelFieldsRequired")); return; @@ -868,7 +889,7 @@ private: 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}); + updated.push_back({id, title, url, zoom ? zoom->value() : 100}); } definitions_ = std::move(updated); reconcile(); diff --git a/src/plugin.c b/src/plugin.c index db8d770..577b22c 100644 --- a/src/plugin.c +++ b/src/plugin.c @@ -68,6 +68,7 @@ struct browser_source { uint32_t width; uint32_t height; uint32_t fps; + uint32_t zoom_percent; bool transparent; bool shutdown_when_hidden; bool visible; @@ -255,6 +256,7 @@ static bool send_initial_config(struct browser_source *browser, struct renderer_ .width = browser->width, .height = browser->height, .fps = browser->fps, + .zoom_percent = browser->zoom_percent, .transparent = browser->transparent, .hardware_acceleration = true, .url_length = (uint32_t)url_length, @@ -434,6 +436,7 @@ static void browser_update(void *data, obs_data_t *settings) browser->width = setting_u32(settings, "width", 2, 8192); browser->height = setting_u32(settings, "height", 2, 8192); browser->fps = setting_u32(settings, "fps", 1, 60); + browser->zoom_percent = setting_u32(settings, "zoom", 25, 500); browser->transparent = obs_data_get_bool(settings, "transparent"); browser->shutdown_when_hidden = obs_data_get_bool(settings, "shutdown_when_hidden"); stop_renderer_locked(browser); @@ -479,6 +482,7 @@ static void browser_defaults(obs_data_t *settings) obs_data_set_default_int(settings, "width", 1280); obs_data_set_default_int(settings, "height", 720); obs_data_set_default_int(settings, "fps", 30); + obs_data_set_default_int(settings, "zoom", 100); obs_data_set_default_bool(settings, "transparent", true); obs_data_set_default_bool(settings, "shutdown_when_hidden", false); } @@ -498,6 +502,7 @@ 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, "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_slider(props, "zoom", obs_module_text("PageZoom"), 25, 500, 5); 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_bool(props, "shutdown_when_hidden", obs_module_text("ShutdownWhenHidden")); diff --git a/src/protocol.h b/src/protocol.h index 16513cc..f65aad6 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -5,7 +5,7 @@ #define OWB_CONFIG_MAGIC 0x4f574243u /* OWBC */ #define OWB_COMMAND_MAGIC 0x4f574245u /* OWBE */ -#define OWB_PROTOCOL_VERSION 2u +#define OWB_PROTOCOL_VERSION 3u #define OWB_MAX_STRING (16u * 1024u * 1024u) #define OWB_SHARED_MAGIC 0x4f574246u /* OWBF */ #define OWB_SHARED_FRAME_FD 198 @@ -17,6 +17,7 @@ struct owb_config_header { uint32_t width; uint32_t height; uint32_t fps; + uint32_t zoom_percent; uint32_t transparent; uint32_t hardware_acceleration; uint32_t url_length; diff --git a/src/renderer.c b/src/renderer.c index 274ebad..e5e39a0 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -35,6 +35,7 @@ struct config { uint32_t width; uint32_t height; uint32_t fps; + uint32_t zoom_percent; bool transparent; char *url; char *css; @@ -107,13 +108,15 @@ static bool read_config(int fd, struct config *config) if (!read_exact(fd, &header, sizeof(header)) || header.magic != OWB_CONFIG_MAGIC || header.version != OWB_PROTOCOL_VERSION || header.width < 2 || header.height < 2 || header.width > 8192 || header.height > 8192 || header.fps < 1 || header.fps > 60 || + header.zoom_percent < 25 || header.zoom_percent > 500 || header.url_length > OWB_MAX_STRING || header.css_length > OWB_MAX_STRING) return false; config->width = header.width; config->height = header.height; config->fps = header.fps; + config->zoom_percent = header.zoom_percent; config->transparent = header.transparent != 0; - /* Protocol v2 retains this legacy flag, but this renderer always enables the + /* The protocol retains this legacy flag, but this renderer always enables the * WebKit compositor. */ (void)header.hardware_acceleration; config->url = calloc((size_t)header.url_length + 1, 1); @@ -709,6 +712,8 @@ static struct renderer *create_renderer(int client_fd, const int descriptors[4]) webkit_settings_set_media_playback_requires_user_gesture(settings, FALSE); webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS); + webkit_web_view_set_zoom_level(renderer->web_view, + (double)renderer->config.zoom_percent / 100.0); webkit_web_view_set_is_muted(renderer->web_view, TRUE); GdkRGBA background = renderer->config.transparent ? (GdkRGBA){0, 0, 0, 0} : (GdkRGBA){1, 1, 1, 1}; webkit_web_view_set_background_color(renderer->web_view, &background); @@ -776,10 +781,10 @@ static struct renderer *create_renderer(int client_fd, const int descriptors[4]) g_source_remove(idle_source); idle_source = 0; } - renderer_log(renderer, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, shared profile, GPU DMA-BUF, XComposite/XShm capture\n", + 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_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); return renderer; fail: diff --git a/tests/module-load-test.c b/tests/module-load-test.c index d221a9f..b7543f6 100644 --- a/tests/module-load-test.c +++ b/tests/module-load-test.c @@ -53,6 +53,7 @@ int main(int argc, char **argv) obs_data_set_int(settings, "width", 64); obs_data_set_int(settings, "height", 64); obs_data_set_int(settings, "fps", 10); + obs_data_set_int(settings, "zoom", 175); obs_data_set_bool(settings, "transparent", true); obs_data_set_bool(settings, "hardware_acceleration", false); obs_source_t *source = obs_source_create_private("webkit_browser_source", "smoke", settings); @@ -61,6 +62,16 @@ int main(int argc, char **argv) fprintf(stderr, "source lifecycle setup failed\n"); valid = false; } + obs_properties_t *properties = source ? obs_source_properties(source) : NULL; + obs_property_t *zoom = properties ? obs_properties_get(properties, "zoom") : 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 (properties) + obs_properties_destroy(properties); if (source) { usleep(500000); obs_source_release(source); diff --git a/tests/protocol-test.c b/tests/protocol-test.c index 0735a4d..4f34072 100644 --- a/tests/protocol-test.c +++ b/tests/protocol-test.c @@ -5,7 +5,8 @@ int main(void) { - assert(sizeof(struct owb_config_header) == 36); + assert(sizeof(struct owb_config_header) == 40); + assert(offsetof(struct owb_config_header, zoom_percent) == 20); assert(sizeof(struct owb_command) == 100); assert(offsetof(struct owb_shared_frames, pixels) == 40); assert(owb_shared_size(2, 2) == 72); diff --git a/tests/renderer-fps-test.c b/tests/renderer-fps-test.c index 9e21589..a22d7d3 100644 --- a/tests/renderer-fps-test.c +++ b/tests/renderer-fps-test.c @@ -109,6 +109,7 @@ int main(int argc, char **argv) struct owb_config_header config = { .magic = OWB_CONFIG_MAGIC, .version = OWB_PROTOCOL_VERSION, .width = width, .height = height, .fps = requested_fps, .transparent = 1, + .zoom_percent = 100, .hardware_acceleration = 1, .url_length = sizeof(url) - 1, .css_length = (uint32_t)css_length, }; diff --git a/tests/renderer-smoke-test.c b/tests/renderer-smoke-test.c index 5bc29d2..979d370 100644 --- a/tests/renderer-smoke-test.c +++ b/tests/renderer-smoke-test.c @@ -56,12 +56,13 @@ int main(int argc, char **argv) { if (argc < 2 || argc > 4) { 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]); return 2; } 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 frames[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) || pipe(frames)) { @@ -108,14 +109,18 @@ int main(int argc, char **argv) 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;" "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 *css = external_url ? "" : opaque ? opaque_css : transparent_css; + const char *css = external_url ? "" : zoom ? zoom_css : opaque ? opaque_css : transparent_css; struct owb_config_header config = { .magic = OWB_CONFIG_MAGIC, .version = OWB_PROTOCOL_VERSION, .width = width, .height = height, .fps = 10, + .zoom_percent = zoom ? 200 : 100, .transparent = opaque ? 0 : 1, /* GPU rendering must still produce an invisible captured frame. */ .hardware_acceleration = 1, @@ -158,10 +163,17 @@ int main(int argc, char **argv) nonuniform ? "content" : "uniform"); continue; } - size_t center = ((size_t)(height / 2) * width + width / 2) * 4; + size_t center = zoom ? ((size_t)5 * width + 15) * 4 + : ((size_t)(height / 2) * width + width / 2) * 4; memcpy(last_bgra, frame + center, sizeof(last_bgra)); - memcpy(last_corner_bgra, frame, sizeof(last_corner_bgra)); - bool expected = opaque + size_t corner = zoom ? ((size_t)5 * width + 25) * 4 : 0; + memcpy(last_corner_bgra, frame + corner, sizeof(last_corner_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[3] == 255 : last_bgra[0] < 8 && last_bgra[1] < 8 && last_bgra[2] > 248 && @@ -192,7 +204,8 @@ int main(int argc, char **argv) fprintf(stderr, "renderer did not produce the expected %s red BGRA frame " "(center BGRA=%u,%u,%u,%u; corner BGRA=%u,%u,%u,%u)\n", - opaque ? "opaque" : "transparent", last_bgra[0], last_bgra[1], + zoom ? "200%-zoomed" : opaque ? "opaque" : "transparent", + last_bgra[0], last_bgra[1], last_bgra[2], last_bgra[3], last_corner_bgra[0], last_corner_bgra[1], last_corner_bgra[2], last_corner_bgra[3]); return 1; diff --git a/tests/shared-profile-test.c b/tests/shared-profile-test.c index 5ee42da..8bba6db 100644 --- a/tests/shared-profile-test.c +++ b/tests/shared-profile-test.c @@ -114,6 +114,7 @@ static bool start_view(struct view *view, const char *renderer, const char *url) 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)) &&