Add sound capture

This commit is contained in:
2026-07-20 10:57:34 +05:00
parent 07bd12ee9a
commit eb13c87dd3
14 changed files with 539 additions and 11 deletions

View File

@@ -1,17 +1,158 @@
#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) {
fprintf(stderr, "usage: %s plugin.so data-directory renderer\n", argv[0]);
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");
@@ -34,26 +175,43 @@ int main(int argc, char **argv)
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_INTERACTION);
(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", "data:text/html,<body style='background:red'></body>");
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);
@@ -64,21 +222,49 @@ int main(int argc, char **argv)
}
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) {
usleep(500000);
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;
}

View File

@@ -5,8 +5,9 @@
int main(void)
{
assert(sizeof(struct owb_config_header) == 40);
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(offsetof(struct owb_shared_frames, pixels) == 40);
assert(owb_shared_size(2, 2) == 72);

View File

@@ -114,6 +114,7 @@ int main(int argc, char **argv)
"background:rgb(241,23,17)}";
const char *url = external_url ? argv[3] : internal_url;
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 = {
.magic = OWB_CONFIG_MAGIC,
.version = OWB_PROTOCOL_VERSION,
@@ -121,12 +122,15 @@ int main(int argc, char **argv)
.height = height,
.fps = 10,
.zoom_percent = zoom ? 200 : 100,
.capture_audio = audio_sink && *audio_sink,
.transparent = opaque ? 0 : 1,
/* GPU rendering must still produce an invisible captured frame. */
.hardware_acceleration = 1,
.url_length = (uint32_t)strlen(url),
.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)) &&
write_exact(control[0], url, strlen(url)) &&
write_exact(control[0], css, strlen(css));