Files
OBS-WebKitGTK/tests/module-load-test.c
2026-07-20 10:57:34 +05:00

271 lines
8.7 KiB
C

#include <obs/obs.h>
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
struct audio_probe {
atomic_uint blocks;
atomic_bool signal;
};
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)
{
if (argc < 4 || argc > 5) {
fprintf(stderr, "usage: %s plugin.so data-directory renderer [audio-test]\n", argv[0]);
return 2;
}
bool audio_test = argc == 5 && strcmp(argv[4], "audio-test") == 0;
char temporary[] = "/tmp/obs-webkit-module-test-XXXXXX";
if (!mkdtemp(temporary)) {
perror("mkdtemp");
return 2;
}
char renderer_link[512];
char locale_link[512];
char locale_source[512];
snprintf(renderer_link, sizeof(renderer_link), "%s/obs-webkit-renderer", temporary);
snprintf(locale_link, sizeof(locale_link), "%s/locale", temporary);
snprintf(locale_source, sizeof(locale_source), "%s/locale", argv[2]);
if (symlink(argv[3], renderer_link) || symlink(locale_source, locale_link)) {
perror("symlink");
return 2;
}
if (!obs_startup("en-US", NULL, NULL)) {
fprintf(stderr, "obs_startup failed\n");
unlink(renderer_link);
unlink(locale_link);
rmdir(temporary);
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;
}
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;
int result = obs_open_module(&module, argv[1], temporary);
if (result != MODULE_SUCCESS || !obs_init_module(module)) {
fprintf(stderr, "module load failed: %d\n", result);
if (audio_server > 0) {
kill(audio_server, SIGTERM);
waitpid(audio_server, NULL, 0);
}
obs_shutdown();
return 1;
}
const char *name = obs_source_get_display_name("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) &&
(flags & OBS_SOURCE_AUDIO) && (flags & OBS_SOURCE_INTERACTION);
if (!valid)
fprintf(stderr, "source registration is incomplete (name=%s, flags=0x%x)\n",
name ? name : "(null)", flags);
obs_data_t *settings = obs_data_create();
obs_data_set_string(settings, "url", audio_test ? audio_url :
"data:text/html,<body style='background:red'></body>");
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, "capture_audio", audio_test);
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);
obs_data_release(settings);
if (!source || obs_source_get_width(source) != 64 || obs_source_get_height(source) != 64) {
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;
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) {
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_shutdown();
unlink(renderer_link);
unlink(locale_link);
rmdir(temporary);
if (audio_server > 0) {
kill(audio_server, SIGTERM);
waitpid(audio_server, NULL, 0);
}
return valid ? 0 : 1;
}