add sync tabs + refresh button
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QBoxLayout>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
@@ -600,6 +601,8 @@ public:
|
||||
/* 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_;
|
||||
@@ -607,22 +610,96 @@ public:
|
||||
|
||||
void setupFrontend()
|
||||
{
|
||||
if (menuAction_ || !QApplication::instance())
|
||||
if (!QApplication::instance())
|
||||
return;
|
||||
auto *main = static_cast<QMainWindow *>(obs_frontend_get_main_window());
|
||||
if (!main)
|
||||
return;
|
||||
menuAction_ = new QAction(text("CustomBrowserDocks"), main);
|
||||
QObject::connect(menuAction_, &QAction::triggered, main, [this] { showEditor(); });
|
||||
if (QMenu *docksMenu = main->findChild<QMenu *>("menuDocks"))
|
||||
docksMenu->addAction(menuAction_);
|
||||
else
|
||||
main->menuBar()->addAction(menuAction_);
|
||||
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)
|
||||
{
|
||||
@@ -803,6 +880,10 @@ private:
|
||||
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;
|
||||
|
||||
490
src/renderer.c
490
src/renderer.c
@@ -14,15 +14,19 @@
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/ipc.h>
|
||||
#include <sys/shm.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "protocol.h"
|
||||
@@ -40,6 +44,13 @@ struct renderer {
|
||||
struct config config;
|
||||
GtkWidget *window;
|
||||
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;
|
||||
size_t shared_size;
|
||||
uint64_t next_sequence;
|
||||
@@ -59,6 +70,22 @@ struct renderer {
|
||||
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)
|
||||
{
|
||||
uint8_t *cursor = data;
|
||||
@@ -74,10 +101,10 @@ static bool read_exact(int fd, void *data, size_t size)
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool read_config(struct config *config)
|
||||
static bool read_config(int fd, struct config *config)
|
||||
{
|
||||
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.width > 8192 || header.height > 8192 || header.fps < 1 || header.fps > 60 ||
|
||||
header.url_length > OWB_MAX_STRING || header.css_length > OWB_MAX_STRING)
|
||||
@@ -93,8 +120,8 @@ static bool read_config(struct config *config)
|
||||
config->css = calloc((size_t)header.css_length + 1, 1);
|
||||
if (!config->url || !config->css)
|
||||
return false;
|
||||
return read_exact(STDIN_FILENO, config->url, header.url_length) &&
|
||||
read_exact(STDIN_FILENO, config->css, header.css_length);
|
||||
return read_exact(fd, config->url, header.url_length) &&
|
||||
read_exact(fd, config->css, header.css_length);
|
||||
}
|
||||
|
||||
static char *normalize_uri(const char *value)
|
||||
@@ -162,10 +189,10 @@ static bool map_shared_frames(struct renderer *renderer)
|
||||
{
|
||||
struct stat info;
|
||||
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;
|
||||
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)
|
||||
return false;
|
||||
return renderer->shared->magic == OWB_SHARED_MAGIC &&
|
||||
@@ -299,7 +326,7 @@ static void publish_frame(struct renderer *renderer, const uint8_t *pixels, int
|
||||
}
|
||||
__atomic_store_n(&renderer->shared->sequence[slot], complete_sequence, __ATOMIC_RELEASE);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -352,7 +379,7 @@ static void *capture_loop(void *opaque)
|
||||
None, None);
|
||||
bool published = publish_x11_frame(renderer);
|
||||
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;
|
||||
}
|
||||
first_frame = false;
|
||||
@@ -498,7 +525,8 @@ static gboolean control_ready(gint fd, GIOCondition condition, gpointer opaque)
|
||||
{
|
||||
struct renderer *renderer = opaque;
|
||||
if (condition & (G_IO_HUP | G_IO_ERR)) {
|
||||
gtk_main_quit();
|
||||
renderer->control_source = 0;
|
||||
destroy_renderer(renderer);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
for (;;) {
|
||||
@@ -509,7 +537,8 @@ static gboolean control_ready(gint fd, GIOCondition condition, gpointer opaque)
|
||||
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
return G_SOURCE_CONTINUE;
|
||||
if (count <= 0) {
|
||||
gtk_main_quit();
|
||||
renderer->control_source = 0;
|
||||
destroy_renderer(renderer);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
renderer->command_used += (size_t)count;
|
||||
@@ -530,22 +559,14 @@ static gboolean clear_window(GtkWidget *widget, cairo_t *context, gpointer unuse
|
||||
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,
|
||||
WebKitJavascriptResult *message, gpointer unused)
|
||||
WebKitJavascriptResult *message, gpointer opaque)
|
||||
{
|
||||
(void)manager;
|
||||
(void)unused;
|
||||
struct renderer *renderer = opaque;
|
||||
JSCValue *value = webkit_javascript_result_get_js_value(message);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -554,34 +575,102 @@ static gboolean load_failed(WebKitWebView *view, WebKitLoadEvent event, const ch
|
||||
{
|
||||
(void)view;
|
||||
(void)event;
|
||||
(void)unused;
|
||||
fprintf(stderr, "Page load failed for %s: %s\n", uri, error->message);
|
||||
struct renderer *renderer = unused;
|
||||
renderer_log(renderer, "Page load failed for %s: %s\n", uri, error->message);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
static gboolean exit_when_idle(gpointer unused)
|
||||
{
|
||||
struct renderer renderer = {0};
|
||||
atomic_init(&renderer.stopping, false);
|
||||
if (!read_config(&renderer.config)) {
|
||||
fprintf(stderr, "Invalid renderer configuration\n");
|
||||
return 2;
|
||||
(void)unused;
|
||||
idle_source = 0;
|
||||
if (!renderers)
|
||||
gtk_main_quit();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void destroy_renderer(struct renderer *renderer)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
static gboolean client_ready(gint fd, GIOCondition condition, gpointer opaque)
|
||||
{
|
||||
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;
|
||||
}
|
||||
prepare_webkit();
|
||||
if (!map_shared_frames(&renderer)) {
|
||||
fprintf(stderr, "Cannot map shared frame transport\n");
|
||||
return 4;
|
||||
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;
|
||||
}
|
||||
if (!gtk_init_check(NULL, NULL)) {
|
||||
fprintf(stderr, "GTK could not connect to the OBS display\n");
|
||||
return 5;
|
||||
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();
|
||||
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"))
|
||||
fprintf(stderr, "Cannot register page diagnostic message handler\n");
|
||||
renderer_log(renderer, "Cannot register page diagnostic message handler\n");
|
||||
static const char error_reporter[] =
|
||||
"(()=>{const p=(k,v)=>{try{window.webkit.messageHandlers.owbLog.postMessage(k+': '+v)}catch{}};"
|
||||
"addEventListener('error',e=>p('error',e.message+' @ '+e.filename+':'+e.lineno));"
|
||||
@@ -592,7 +681,7 @@ int main(void)
|
||||
NULL, NULL);
|
||||
webkit_user_content_manager_add_script(manager, reporter);
|
||||
webkit_user_script_unref(reporter);
|
||||
if (renderer.config.transparent) {
|
||||
if (renderer->config.transparent) {
|
||||
static const char transparent_page_css[] =
|
||||
"html,body{background:transparent!important}";
|
||||
WebKitUserStyleSheet *transparent_sheet = webkit_user_style_sheet_new(
|
||||
@@ -601,96 +690,303 @@ int main(void)
|
||||
webkit_user_content_manager_add_style_sheet(manager, transparent_sheet);
|
||||
webkit_user_style_sheet_unref(transparent_sheet);
|
||||
}
|
||||
if (*renderer.config.css) {
|
||||
WebKitUserStyleSheet *sheet = webkit_user_style_sheet_new(renderer.config.css,
|
||||
if (*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_manager_add_style_sheet(manager, sheet);
|
||||
webkit_user_style_sheet_unref(sheet);
|
||||
}
|
||||
renderer.web_view = WEBKIT_WEB_VIEW(webkit_web_view_new_with_user_content_manager(manager));
|
||||
g_signal_connect(renderer.web_view, "load-failed", G_CALLBACK(load_failed), NULL);
|
||||
renderer->web_view = WEBKIT_WEB_VIEW(g_object_new(
|
||||
WEBKIT_TYPE_WEB_VIEW, "web-context", shared_context,
|
||||
"user-content-manager", manager, NULL));
|
||||
g_signal_connect(renderer->web_view, "load-failed", G_CALLBACK(load_failed), renderer);
|
||||
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_page_cache(settings, TRUE);
|
||||
webkit_settings_set_enable_smooth_scrolling(settings, TRUE);
|
||||
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_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);
|
||||
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);
|
||||
|
||||
renderer.window = gtk_window_new(GTK_WINDOW_POPUP);
|
||||
renderer->window = gtk_window_new(GTK_WINDOW_POPUP);
|
||||
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);
|
||||
if (visual)
|
||||
gtk_widget_set_visual(renderer.window, visual);
|
||||
gtk_widget_set_app_paintable(renderer.window, renderer.config.transparent);
|
||||
if (renderer.config.transparent)
|
||||
g_signal_connect(renderer.window, "draw", G_CALLBACK(clear_window), NULL);
|
||||
gtk_window_set_decorated(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_pager_hint(GTK_WINDOW(renderer.window), TRUE);
|
||||
gtk_window_set_accept_focus(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_set_default_size(GTK_WINDOW(renderer.window), (int)renderer.config.width,
|
||||
(int)renderer.config.height);
|
||||
gtk_widget_set_size_request(GTK_WIDGET(renderer.web_view), (int)renderer.config.width,
|
||||
(int)renderer.config.height);
|
||||
gtk_container_add(GTK_CONTAINER(renderer.window), GTK_WIDGET(renderer.web_view));
|
||||
gtk_widget_realize(renderer.window);
|
||||
GdkWindow *renderer_window = gtk_widget_get_window(renderer.window);
|
||||
gtk_widget_set_visual(renderer->window, visual);
|
||||
gtk_widget_set_app_paintable(renderer->window, renderer->config.transparent);
|
||||
if (renderer->config.transparent)
|
||||
g_signal_connect(renderer->window, "draw", G_CALLBACK(clear_window), NULL);
|
||||
gtk_window_set_decorated(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_pager_hint(GTK_WINDOW(renderer->window), TRUE);
|
||||
gtk_window_set_accept_focus(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_set_default_size(GTK_WINDOW(renderer->window), (int)renderer->config.width,
|
||||
(int)renderer->config.height);
|
||||
gtk_widget_set_size_request(GTK_WIDGET(renderer->web_view), (int)renderer->config.width,
|
||||
(int)renderer->config.height);
|
||||
gtk_container_add(GTK_CONTAINER(renderer->window), GTK_WIDGET(renderer->web_view));
|
||||
gtk_widget_realize(renderer->window);
|
||||
GdkWindow *renderer_window = gtk_widget_get_window(renderer->window);
|
||||
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");
|
||||
return 6;
|
||||
renderer_log(renderer, "GTK did not create the native window required for GPU rendering\n");
|
||||
goto fail;
|
||||
}
|
||||
cairo_region_t *empty_input = cairo_region_create();
|
||||
gdk_window_input_shape_combine_region(renderer_window, empty_input, 0, 0);
|
||||
cairo_region_destroy(empty_input);
|
||||
gdk_window_set_opacity(renderer_window, 0.0);
|
||||
gtk_widget_show_all(renderer.window);
|
||||
gtk_widget_show_all(renderer->window);
|
||||
while (gtk_events_pending())
|
||||
gtk_main_iteration();
|
||||
if (!initialize_x11_capture(&renderer)) {
|
||||
fprintf(stderr, "Cannot initialize XComposite/XDamage/XShm GPU capture\n");
|
||||
return 7;
|
||||
if (!initialize_x11_capture(renderer)) {
|
||||
renderer_log(renderer, "Cannot initialize XComposite/XDamage/XShm GPU capture\n");
|
||||
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)
|
||||
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)
|
||||
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
|
||||
g_unix_fd_add(STDIN_FILENO, G_IO_IN | G_IO_HUP | G_IO_ERR, control_ready, &renderer);
|
||||
char *uri = normalize_uri(renderer.config.url);
|
||||
webkit_web_view_load_uri(renderer.web_view, uri);
|
||||
fcntl(renderer->control_fd, F_SETFL, flags | O_NONBLOCK);
|
||||
renderer->control_source = g_unix_fd_add(renderer->control_fd,
|
||||
G_IO_IN | G_IO_HUP | G_IO_ERR, control_ready, renderer);
|
||||
renderer->client_source = g_unix_fd_add(renderer->client_fd,
|
||||
G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, client_ready, renderer);
|
||||
char *uri = normalize_uri(renderer->config.url);
|
||||
webkit_web_view_load_uri(renderer->web_view, uri);
|
||||
g_free(uri);
|
||||
if (pthread_create(&renderer.capture_thread, NULL, capture_loop, &renderer) != 0) {
|
||||
fprintf(stderr, "Cannot start GPU capture thread\n");
|
||||
return 8;
|
||||
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;
|
||||
fprintf(stderr, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, GPU DMA-BUF, XComposite/XShm capture\n",
|
||||
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, 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);
|
||||
fflush(stderr);
|
||||
g_unix_signal_add(SIGTERM, terminate_renderer, &renderer);
|
||||
g_unix_signal_add(SIGINT, terminate_renderer, &renderer);
|
||||
webkit_get_minor_version(), webkit_get_micro_version(), renderer->config.width,
|
||||
renderer->config.height, renderer->config.fps);
|
||||
return 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();
|
||||
|
||||
atomic_store_explicit(&renderer.stopping, true, memory_order_release);
|
||||
if (renderer.capture_thread_started)
|
||||
pthread_join(renderer.capture_thread, NULL);
|
||||
destroy_x11_capture(&renderer);
|
||||
munmap(renderer.shared, renderer.shared_size);
|
||||
free(renderer.config.url);
|
||||
free(renderer.config.css);
|
||||
while (renderers)
|
||||
destroy_renderer(renderers->data);
|
||||
if (idle_source) {
|
||||
g_source_remove(idle_source);
|
||||
idle_source = 0;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user