#include #include #include #include #include #include #include #include #include #include #include #include #include #include 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[] = "" "" ""; 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,"); 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; }