Add sound capture
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user