From c22cd0fb5f9b1132af93686be09c7ed69cd89b75 Mon Sep 17 00:00:00 2001 From: Joe Kaushal Date: Wed, 25 Aug 2021 20:50:16 +0100 Subject: [PATCH] Rewrite with ActivateAudioInterfaceAsync --- CMakeLists.txt | 16 +- README.md | 13 +- data/locale/en-GB.ini | 12 +- src/audio-capture-helper/CMakeLists.txt | 10 + .../audio-capture-helper.c | 529 ++++++++ .../audio-capture-helper.h | 39 + src/audio-capture-helper/format-conversion.h | 66 + src/audio-capture-helper/uuids.cpp | 24 + .../uuids.h | 13 +- src/audio-capture.c | 1068 +++++------------ src/audio-capture.h | 148 +++ src/audio-hook/CMakeLists.txt | 23 - src/audio-hook/audio-hook.c | 481 -------- src/common.h | 79 ++ src/get-audio-offsets/CMakeLists.txt | 16 - src/get-audio-offsets/get-audio-offsets.c | 228 ---- src/get-audio-offsets/uuids.cpp | 17 - src/hook-info.h | 116 -- src/inject-helper/CMakeLists.txt | 24 - src/inject-helper/inject-helper.c | 122 -- src/inject-library.c | 150 --- src/inject-library.h | 20 - src/plugin.c | 167 +-- src/window-helpers.c | 496 ++++---- src/window-helpers.h | 23 +- 25 files changed, 1442 insertions(+), 2458 deletions(-) create mode 100644 src/audio-capture-helper/CMakeLists.txt create mode 100644 src/audio-capture-helper/audio-capture-helper.c create mode 100644 src/audio-capture-helper/audio-capture-helper.h create mode 100644 src/audio-capture-helper/format-conversion.h create mode 100644 src/audio-capture-helper/uuids.cpp rename src/{get-audio-offsets => audio-capture-helper}/uuids.h (53%) create mode 100644 src/audio-capture.h delete mode 100644 src/audio-hook/CMakeLists.txt delete mode 100644 src/audio-hook/audio-hook.c create mode 100644 src/common.h delete mode 100644 src/get-audio-offsets/CMakeLists.txt delete mode 100644 src/get-audio-offsets/get-audio-offsets.c delete mode 100644 src/get-audio-offsets/uuids.cpp delete mode 100644 src/hook-info.h delete mode 100644 src/inject-helper/CMakeLists.txt delete mode 100644 src/inject-helper/inject-helper.c delete mode 100644 src/inject-library.c delete mode 100644 src/inject-library.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a1f24b..6e1f166 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(win-capture-audio VERSION 1.0.0) +project(win-capture-audio VERSION 2.0.0) set(PLUGIN_AUTHOR "bozbez") set(RELEASE_DIR "${PROJECT_SOURCE_DIR}/release") @@ -22,12 +22,10 @@ set(win-capture-audio_SOURCES src/plugin.c src/audio-capture.c src/obfuscate.c - src/window-helpers.c - src/inject-library.c) + src/window-helpers.c) add_library(win-capture-audio MODULE ${win-capture-audio_SOURCES}) -target_link_libraries(win-capture-audio libobs dwmapi psapi ksuser) - +target_link_libraries(win-capture-audio libobs dwmapi psapi) add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD COMMAND if $,$>==1 ( @@ -81,10 +79,4 @@ function(copy_helper_lib) ) endfunction() - -add_subdirectory(src/get-audio-offsets) -add_subdirectory(src/audio-hook) -add_subdirectory(src/inject-helper) - -add_custom_target(all-no-plugin) -add_dependencies(all-no-plugin get-audio-offsets audio-hook inject-helper) \ No newline at end of file +add_subdirectory(src/audio-capture-helper) \ No newline at end of file diff --git a/README.md b/README.md index 4573d31..76c4d3b 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,11 @@ # win-capture-audio -An OBS plugin based on OBS's win-capture/game-capture that hooks WASAPI's audio output functions (rather than the various graphics API funcitons) that enables capture of audio streams directly from applications. This eliminates the need for third-party software or hardware audio mixing tools that introduce complexity, and in the case of software tools introduce mandatory latency. +An OBS plugin similar to OBS's win-capture/game-capture that uses [ActivateAudioInterfaceAsync](https://docs.microsoft.com/en-us/windows/win32/api/mmdeviceapi/nf-mmdeviceapi-activateaudiointerfaceasync) with [AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS](https://docs.microsoft.com/en-us/windows/win32/api/audioclientactivationparams/ns-audioclientactivationparams-audioclient_process_loopback_params) to capture audio output from a specific process (and optionally its tree of child processes). This eliminates the need for third-party software or hardware audio mixing tools that introduce complexity, and in the case of software tools introduce mandatory latency. -The modus operandi is identical to the aforementioned game-capture plugin (and most likely to Discord's solution), and is inherently liable to instability and other issues due to the lack of a more "official" solution from the Windows API. - -WARNING: I am not able to guarantee that using this is anti-cheat safe, however similar hook methods are employed in many widely deployed applications (Discord, Steam Overlay, RTSS, NVIDIA's ShadowPlay, etc...). +**This plugin requires an updated version of Windows 10 2004 (released 2020-05-27) or later.** ![overview](https://raw.githubusercontent.com/bozbez/win-capture-audio/main/media/overview.png) -## Limitations (current) - -- WASAPI only (no DirectSound, WaveOut, etc...) -- No Windows App support (probably?) -- Chrome and Chrome-based (e.g. Electron) applications don't work (probably a limitation of the process selection logic rather than the hooking) -- Directly conflicts with Discord streaming (and maybe ShadowPlay) (unresolvable?) - ## Installation and Usage 1. Head over to the [Releases](https://github.com/bozbez/win-capture-audio/releases) page and download the latest installer (or zip if you are using a portable installation) diff --git a/data/locale/en-GB.ini b/data/locale/en-GB.ini index c179ab1..cd37e64 100644 --- a/data/locale/en-GB.ini +++ b/data/locale/en-GB.ini @@ -11,10 +11,10 @@ Window.Priority.Exe="Match title, otherwise find window of same executable" Hotkey.Start="Capture foreground window" Hotkey.Stop="Deactivate capture" -UseIndirectHook="Use anti-cheat compatibility hook" +IncludeProcessTree="Include process tree" -HookRate="Hook Rate" -HookRate.Slow="Slow" -HookRate.Normal="Normal (recommended)" -HookRate.Fast="Fast" -HookRate.Fastest="Fastest" \ No newline at end of file +RecaptureRate="Recapture Rate" +RecaptureRate.Slow="Slow" +RecaptureRate.Normal="Normal (recommended)" +RecaptureRate.Fast="Fast" +RecaptureRate.Fastest="Fastest" \ No newline at end of file diff --git a/src/audio-capture-helper/CMakeLists.txt b/src/audio-capture-helper/CMakeLists.txt new file mode 100644 index 0000000..f0c7f57 --- /dev/null +++ b/src/audio-capture-helper/CMakeLists.txt @@ -0,0 +1,10 @@ +project(audio-capture-helper) + +set(audio-capture-helper_SOURCES audio-capture-helper.c uuids.cpp) +add_executable(audio-capture-helper ${audio-capture-helper_SOURCES}) +target_link_libraries(audio-capture-helper libobs ksuser mmdevapi) + +set_target_properties(audio-capture-helper PROPERTIES + OUTPUT_NAME "audio-capture-helper") + +copy_helper_lib(audio-capture-helper) \ No newline at end of file diff --git a/src/audio-capture-helper/audio-capture-helper.c b/src/audio-capture-helper/audio-capture-helper.c new file mode 100644 index 0000000..5621d99 --- /dev/null +++ b/src/audio-capture-helper/audio-capture-helper.c @@ -0,0 +1,529 @@ +#include + +#include +#include +#include + +#include +#include +#include + +#include "../common.h" + +#include "audio-capture-helper.h" +#include "format-conversion.h" +#include "uuids.h" + +static HRESULT STDMETHODCALLTYPE +dummy_query_interface(IActivateAudioInterfaceCompletionHandler *this, + REFIID riid, _COM_Outptr_ void **ppv_object) +{ + *ppv_object = this; + return S_OK; +} + +static ULONG STDMETHODCALLTYPE +dummy_add_release(IActivateAudioInterfaceCompletionHandler *this) +{ + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE +activate_completed(IActivateAudioInterfaceCompletionHandler *this, + IActivateAudioInterfaceAsyncOperation *operation); + +static IActivateAudioInterfaceCompletionHandlerVtbl completion_handler_vtbl = { + .QueryInterface = dummy_query_interface, + .AddRef = dummy_add_release, + .Release = dummy_add_release, + .ActivateCompleted = activate_completed}; + +typedef struct completion_handler { + IActivateAudioInterfaceCompletionHandlerVtbl *lpVtbl; + IAudioClient *client; + + HANDLE event_finished; + HRESULT activate_hr; +} completion_handler_t; + +static HRESULT STDMETHODCALLTYPE +activate_completed(IActivateAudioInterfaceCompletionHandler *this, + IActivateAudioInterfaceAsyncOperation *operation) +{ + completion_handler_t *completion_handler = (completion_handler_t *)this; + completion_handler->activate_hr = S_OK; + + HRESULT activate_hr = E_FAIL; + HRESULT hr = E_FAIL; + + hr = CALL(operation, GetActivateResult, &activate_hr, + (IUnknown **)&completion_handler->client); + + if (FAILED(hr)) { + error("failed to get activate result (0x%lx)", hr); + completion_handler->activate_hr = hr; + } + + if (FAILED(activate_hr)) { + error("activate failed (0x%lx)", activate_hr); + completion_handler->activate_hr = activate_hr; + } + + SetEvent(completion_handler->event_finished); + return S_OK; +} + +static AUDIOCLIENT_ACTIVATION_PARAMS get_params(DWORD pid, bool include_tree) +{ + int mode = include_tree + ? PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE + : PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE; + + AUDIOCLIENT_ACTIVATION_PARAMS client_params = { + .ActivationType = AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK, + .ProcessLoopbackParams.TargetProcessId = pid, + .ProcessLoopbackParams.ProcessLoopbackMode = mode, + }; + + return client_params; +} + +static PROPVARIANT get_propvariant(AUDIOCLIENT_ACTIVATION_PARAMS *params) +{ + PROPVARIANT propvariant = { + .vt = VT_BLOB, + .blob.cbSize = sizeof(*params), + .blob.pBlobData = (BYTE *)params, + }; + + return propvariant; +} + +static IAudioClient *get_audio_client(DWORD pid, bool include_tree) +{ + audio_uuids_t uuids = get_uuids(); + + AUDIOCLIENT_ACTIVATION_PARAMS params = get_params(pid, include_tree); + PROPVARIANT propvariant = get_propvariant(¶ms); + + HRESULT hr = E_FAIL; + + IActivateAudioInterfaceAsyncOperation *async_op; + completion_handler_t completion_handler = { + .lpVtbl = &completion_handler_vtbl, + .client = NULL, + + .event_finished = CreateEventW(NULL, FALSE, FALSE, NULL), + .activate_hr = E_FAIL, + }; + + if (completion_handler.event_finished == NULL) { + error("failed to create completion event"); + return NULL; + } + + hr = ActivateAudioInterfaceAsync( + VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK, &uuids.IID_IAudioClient, + &propvariant, + (IActivateAudioInterfaceCompletionHandler *)&completion_handler, + &async_op); + + if (FAILED(hr)) { + error("failed to activate audio interface (0x%lx)", hr); + return NULL; + } + + WaitForSingleObject(completion_handler.event_finished, INFINITE); + if (FAILED(completion_handler.activate_hr)) { + error("activate completion handler failed (0x%lx)", + completion_handler.activate_hr); + SAFE_RELEASE(completion_handler.client); + return NULL; + } + + return completion_handler.client; +} + +static WAVEFORMATEX *get_default_mix_format() +{ + audio_uuids_t uuids = get_uuids(); + HRESULT hr; + + WAVEFORMATEX *format = NULL; + + IMMDeviceEnumerator *enumerator = NULL; + hr = CoCreateInstance(&uuids.CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, + &uuids.IID_IMMDeviceEnumerator, + (void **)&enumerator); + + if (FAILED(hr)) { + error("failed to create device enumerator (0x%lx)", hr); + goto exit; + } + + IMMDevice *device = NULL; + hr = CALL(enumerator, GetDefaultAudioEndpoint, eRender, eMultimedia, + &device); + + if (FAILED(hr)) { + error("failed to get default audio endpoint (0x%lx)", hr); + goto exit; + } + + IAudioClient *client = NULL; + hr = CALL(device, Activate, &uuids.IID_IAudioClient, CLSCTX_ALL, NULL, + (void **)&client); + + if (FAILED(hr)) { + error("failed to activate device (0x%lx)", hr); + goto exit; + } + + hr = CALL(client, GetMixFormat, &format); + + if (FAILED(hr)) { + error("failed to get device mix format (0x%lx)", hr); + goto exit; + } + +exit: + SAFE_RELEASE(client); + SAFE_RELEASE(device); + SAFE_RELEASE(enumerator); + + return format; +} + +static WAVEFORMATEX *setup_audio_client(IAudioClient *client, HANDLE event) +{ + WAVEFORMATEX *format = get_default_mix_format(); + if (format == NULL) { + error("failed to get default render mix format"); + SAFE_RELEASE(client); + return NULL; + } + + HRESULT hr = E_FAIL; + hr = CALL(client, Initialize, AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_LOOPBACK | + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + 0, 0, format, NULL); + if (FAILED(hr)) { + error("failed to initialize audio client (0x%lx)", hr); + SAFE_RELEASE(client); + return NULL; + } + + hr = CALL(client, SetEventHandle, event); + if (FAILED(hr)) { + error("failed to set audio client event handle (0x%lx)", hr); + SAFE_RELEASE(client); + } + + return format; +} + +static IAudioCaptureClient *get_capture_client(IAudioClient *client) +{ + audio_uuids_t uuids = get_uuids(); + + IAudioCaptureClient *capture_client; + HRESULT hr = CALL(client, GetService, &uuids.IID_IAudioCaptureClient, + (void **)&capture_client); + + if (FAILED(hr)) { + error("failed get capture client (0x%lx)", hr); + return NULL; + } + + return capture_client; +} + +static int parse_options(capture_options_t *options, int argc, char *argv[]) +{ + if (argc != 4) + return 100; + + options->pid = strtoul(argv[1], NULL, 0); + if (options->pid == 0) + return 101; + + if (strcmp("include", argv[2]) == 0) + options->include_tree = true; + else if (strcmp("exclude", argv[2]) == 0) + options->include_tree = false; + else + return 102; + + options->tag = argv[3]; + if (strlen(options->tag) < 1) + return 103; + + return 0; +} + +static void destroy_helper_data(audio_capture_helper_context_t *ctx) +{ + if (ctx->data != NULL) { + UnmapViewOfFile((void *)ctx->data); + ctx->data = NULL; + } + + safe_close_handle(&ctx->data_map); +} + +static bool init_helper_data(audio_capture_helper_context_t *ctx) +{ + wchar_t name[MAX_PATH]; + format_name_tag(name, HELPER_DATA_NAME, ctx->options.tag); + + ctx->data_map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name); + if (ctx->data_map == NULL) { + error("failed to open file mapping with name: %ls", name); + return false; + } + + ctx->data = MapViewOfFile(ctx->data_map, FILE_MAP_ALL_ACCESS, 0, 0, + sizeof(audio_capture_helper_data_t)); + if (ctx->data == NULL) { + error("failed to create file map view"); + return false; + } + + ctx->data->audio.data[0] = (uint8_t *)ctx->data->data; + + ctx->data->audio.speakers = + get_obs_speaker_layout((WAVEFORMATEXTENSIBLE *)ctx->format); + ctx->data->audio.format = + get_obs_format((WAVEFORMATEXTENSIBLE *)ctx->format); + ctx->data->audio.samples_per_sec = ctx->format->nSamplesPerSec; + + return true; +} + +static void destroy_events(audio_capture_helper_context_t *ctx) +{ + for (int i = 0; i < NUM_HELPER_EVENTS_TOTAL; ++i) + safe_close_handle(&ctx->events[i]); +} + +static bool init_events(audio_capture_helper_context_t *ctx) +{ + for (int i = 0; i < NUM_HELPER_EVENTS_TOTAL; ++i) { + wchar_t name[MAX_PATH]; + format_name_tag(name, event_names[i], ctx->options.tag); + + ctx->events[i] = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, + false, name); + + if (ctx->events[i] == NULL) + return false; + } + + return true; +} + +static void destroy_context(audio_capture_helper_context_t *ctx) +{ + SAFE_RELEASE(ctx->client); + SAFE_RELEASE(ctx->capture_client); + + safe_close_handle(&ctx->event_data); + + destroy_helper_data(ctx); + destroy_events(ctx); +} + +static int init_context(audio_capture_helper_context_t *ctx, int argc, + char *argv[]) +{ + int ret = 0; + + ret = parse_options(&ctx->options, argc, argv); + if (ret != 0) { + error("failed to parse command line options"); + return ret; + } + + ctx->client = + get_audio_client(ctx->options.pid, ctx->options.include_tree); + if (ctx->client == NULL) { + error("failed to get audio client"); + ret = 110; + goto err; + } + + ctx->event_data = CreateEventW(NULL, FALSE, FALSE, NULL); + if (ctx->event_data == NULL) { + error("failed to create data event"); + ret = 111; + goto err; + } + + ctx->format = setup_audio_client(ctx->client, ctx->event_data); + if (ctx->format == NULL) { + error("failed to setup audio client"); + ret = 112; + goto err; + } + + ctx->capture_client = get_capture_client(ctx->client); + if (ctx->capture_client == NULL) { + error("failed to get capture client"); + ret = 113; + goto err; + } + + if (!init_helper_data(ctx)) { + error("failed to init helper shmem"); + ret = 114; + goto err; + } + + if (!init_events(ctx)) { + error("failed to init events"); + ret = 115; + goto err; + } + + return ret; + +err: + destroy_context(ctx); + return ret; +} + +static bool forward_audio_packet(audio_capture_helper_context_t *ctx) +{ + if (InterlockedCompareExchange(&ctx->data->lock, 1, 0) != 0) { + warn("failed to acquire data lock"); + return true; + } + + HRESULT hr; + + ctx->data->audio.frames = 0; + ctx->data->data_size = 0; + + size_t frame_size = + (ctx->format->wBitsPerSample * ctx->format->nChannels) / + CHAR_BIT; + + // Real number obtained from first GetBuffer + UINT32 num_frames = 1; + + while (num_frames > 0) { + BYTE *data; + DWORD flags; + UINT64 qpc_position; + + hr = CALL(ctx->capture_client, GetBuffer, &data, &num_frames, + &flags, NULL, &qpc_position); + if (FAILED(hr)) { + warn("capture client getbuffer failed"); + InterlockedExchange(&ctx->data->lock, 0); + return false; + } + + // Set timestamp only on first GetBuffer + if (ctx->data->audio.frames == 0) + ctx->data->audio.timestamp = qpc_position * 100; + + size_t packet_size = frame_size * num_frames; + size_t data_start = ctx->data->data_size; + + for (size_t i = 0; i < packet_size; ++i) { + ctx->data->data[data_start + i] = + flags & AUDCLNT_BUFFERFLAGS_SILENT ? 0 + : data[i]; + } + + ctx->data->data_size += packet_size; + ctx->data->audio.frames += num_frames; + + hr = CALL(ctx->capture_client, ReleaseBuffer, num_frames); + if (FAILED(hr)) + warn("capture client releasebuffer failed"); + + hr = CALL(ctx->capture_client, GetNextPacketSize, &num_frames); + if (FAILED(hr)) { + warn("capture client getnextpacketsize failed"); + num_frames = 0; + } + } + + InterlockedExchange(&ctx->data->lock, 0); + SetEvent(ctx->events[HELPER_EVENT_DATA]); + + return true; +} + +static bool tick(audio_capture_helper_context_t *ctx, int event_id, int *err) +{ + switch (event_id) { + case HELPER_WO_EVENT_SHUTDOWN: + info("shutting down"); + return true; + case NUM_HELPER_WO_EVENTS: // event_data + return !forward_audio_packet(ctx); + break; + default: + error("unexpected event id: %d", event_id); + *err = 4; + return true; + } + + return false; +} + +int main(int argc, char *argv[]) +{ + HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); + if (FAILED(hr)) { + error("failed to initialize COM runtime"); + return 1; + } + + int ret = 0; + + audio_capture_helper_context_t ctx; + ret = init_context(&ctx, argc, argv); + if (ret != 0) { + error("failed to init context"); + return ret; + } + + info("capture initialized"); + + int num_events = 1 + NUM_HELPER_WO_EVENTS; + HANDLE *events = malloc(num_events * sizeof(HANDLE)); + for (int i = 0; i < NUM_HELPER_WO_EVENTS; ++i) + events[i] = ctx.events[i]; + + events[num_events - 1] = ctx.event_data; + + CALL(ctx.client, Start); + + bool shutdown = false; + while (!shutdown) { + int event_id = WaitForMultipleObjects(num_events, events, false, + INFINITE); + + if (!(event_id >= WAIT_OBJECT_0 && + event_id < WAIT_OBJECT_0 + num_events)) { + error("error waiting for events"); + + ret = 2; + shutdown = true; + } + + shutdown = tick(&ctx, event_id, &ret); + } + + CALL(ctx.client, Stop); + + free(events); + destroy_context(&ctx); + + return ret; +} \ No newline at end of file diff --git a/src/audio-capture-helper/audio-capture-helper.h b/src/audio-capture-helper/audio-capture-helper.h new file mode 100644 index 0000000..a6200b8 --- /dev/null +++ b/src/audio-capture-helper/audio-capture-helper.h @@ -0,0 +1,39 @@ +#include + +#include +#include + +#include "../common.h" + +#define do_log(format, ...) fprintf(stderr, format, ##__VA_ARGS__) + +#define error(format, ...) \ + do_log("error: (%s): " format "\n", __func__, ##__VA_ARGS__) +#define warn(format, ...) \ + do_log("warn: (%s): " format "\n", __func__, ##__VA_ARGS__) +#define info(format, ...) \ + do_log("info: (%s): " format "\n", __func__, ##__VA_ARGS__) +#define debug(format, ...) \ + do_log("debug: (%s): " format "\n", __func__, ##__VA_ARGS__) + +typedef struct capture_options { + DWORD pid; + bool include_tree; + + char *tag; +} capture_options_t; + +typedef struct audio_capture_helper_context { + capture_options_t options; + + IAudioClient *client; + IAudioCaptureClient *capture_client; + + WAVEFORMATEX *format; + HANDLE event_data; + + HANDLE events[NUM_HELPER_EVENTS_TOTAL]; + + HANDLE data_map; + volatile audio_capture_helper_data_t *data; +} audio_capture_helper_context_t; diff --git a/src/audio-capture-helper/format-conversion.h b/src/audio-capture-helper/format-conversion.h new file mode 100644 index 0000000..4e78af7 --- /dev/null +++ b/src/audio-capture-helper/format-conversion.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include + +#include + +static inline enum speaker_layout +get_obs_speaker_layout(WAVEFORMATEXTENSIBLE *format) +{ + switch (format->Format.nChannels) { + case 1: + return SPEAKERS_MONO; + case 2: + return SPEAKERS_STEREO; + case 3: + return SPEAKERS_2POINT1; + case 4: + return SPEAKERS_4POINT0; + case 5: + return SPEAKERS_4POINT1; + case 6: + return SPEAKERS_5POINT1; + case 8: + return SPEAKERS_7POINT1; + } + + return SPEAKERS_UNKNOWN; +} + +static inline enum audio_format get_obs_pcm_format(int bits_per_sample) +{ + switch (bits_per_sample) { + case 8: + return AUDIO_FORMAT_U8BIT; + case 16: + return AUDIO_FORMAT_16BIT; + case 32: + return AUDIO_FORMAT_32BIT; + }; + + return AUDIO_FORMAT_UNKNOWN; +} + +static inline enum audio_format get_obs_format(WAVEFORMATEXTENSIBLE *format) +{ + switch (format->Format.wFormatTag) { + case WAVE_FORMAT_PCM: + return get_obs_pcm_format(format->Format.wBitsPerSample); + + case WAVE_FORMAT_IEEE_FLOAT: + return AUDIO_FORMAT_FLOAT; + + case WAVE_FORMAT_EXTENSIBLE: + if (IsEqualGUID(&format->SubFormat, + &KSDATAFORMAT_SUBTYPE_PCM)) { + return get_obs_pcm_format( + format->Format.wBitsPerSample); + } else if (IsEqualGUID(&format->SubFormat, + &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) { + return AUDIO_FORMAT_FLOAT; + } + } + + return AUDIO_FORMAT_UNKNOWN; +} \ No newline at end of file diff --git a/src/audio-capture-helper/uuids.cpp b/src/audio-capture-helper/uuids.cpp new file mode 100644 index 0000000..4f13c58 --- /dev/null +++ b/src/audio-capture-helper/uuids.cpp @@ -0,0 +1,24 @@ +#include +#include +#include + +#include "uuids.h" + +extern "C" { + +audio_uuids get_uuids() +{ + audio_uuids uuids; + + uuids.CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); + uuids.IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); + + uuids.IID_IAudioClient = __uuidof(IAudioClient); + uuids.IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient); + + uuids.IID_IAudioSessionControl = __uuidof(IAudioSessionControl); + uuids.IID_IAudioSessionControl2 = __uuidof(IAudioSessionControl2); + + return uuids; +} +} diff --git a/src/get-audio-offsets/uuids.h b/src/audio-capture-helper/uuids.h similarity index 53% rename from src/get-audio-offsets/uuids.h rename to src/audio-capture-helper/uuids.h index 788d86a..12a7918 100644 --- a/src/get-audio-offsets/uuids.h +++ b/src/audio-capture-helper/uuids.h @@ -6,14 +6,19 @@ extern "C" { #endif -typedef struct uuids { +typedef struct audio_uuids { CLSID CLSID_MMDeviceEnumerator; IID IID_IMMDeviceEnumerator; + IID IID_IAudioClient; - IID IID_IAudioRenderClient; -} uuids_t; + IID IID_IAudioCaptureClient; + + IID IID_IAudioSessionControl; + IID IID_IAudioSessionControl2; + +} audio_uuids_t; -uuids_t get_uuids(); +audio_uuids_t get_uuids(); #ifdef __cplusplus } diff --git a/src/audio-capture.c b/src/audio-capture.c index 20dfe57..ad24313 100644 --- a/src/audio-capture.c +++ b/src/audio-capture.c @@ -1,219 +1,32 @@ +#include +#include +#include +#include #include #include #include -#include + #include +#include +#include -#include #include #include #include #include #include -#include "media-io/audio-io.h" -#include "util/base.h" #include "window-helpers.h" -#include "hook-info.h" +#include "audio-capture.h" #include "obfuscate.h" -#include "inject-library.h" - -#define do_log(level, format, ...) \ - do_log_source(ctx->source, level, "(%s) " format, __func__, \ - ##__VA_ARGS__) - -inline static void do_log_source(const obs_source_t *source, int level, - const char *format, ...) -{ - va_list args; - va_start(args, format); - - const char *name = obs_source_get_name(source); - int len = strlen(name); - - const char *format_source = len <= 8 ? "[audio-capture: '%s'] %s" - : "[audio-capture: '%.8s...'] %s"; - - int len_full = strlen(format_source) + 12 + strlen(format); - char *format_full = bzalloc(len_full); - - snprintf(format_full, len_full, format_source, name, format); - blogva(level, format_full, args); - - bfree(format_full); - va_end(args); -} - -#define error(format, ...) do_log(LOG_ERROR, format, ##__VA_ARGS__) -#define warn(format, ...) do_log(LOG_WARNING, format, ##__VA_ARGS__) -#define info(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__) -#define debug(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__) - -/* clang-format off */ - -#define SETTING_MODE "mode" - -#define SETTING_WINDOW "window" -#define SETTING_WINDOW_PRIORITY "window_priority" - -#define SETTING_USE_INDIRECT_HOOK "use_indirect_hook" -#define SETTING_HOOK_RATE "hook_rate" - -#define TEXT_MODE obs_module_text("Mode") -#define TEXT_MODE_WINDOW obs_module_text("Mode.Window") -#define TEXT_MODE_HOTKEY obs_module_text("Mode.Hotkey") - -#define TEXT_WINDOW obs_module_text("Window") -#define TEXT_WINDOW_PRIORITY obs_module_text("Window.Priority") -#define TEXT_WINDOW_PRIORITY_TITLE obs_module_text("Window.Priority.Title") -#define TEXT_WINDOW_PRIORITY_CLASS obs_module_text("Window.Priority.Class") -#define TEXT_WINDOW_PRIORITY_EXE obs_module_text("Window.Priority.Exe") - -#define TEXT_HOTKEY_START obs_module_text("Hotkey.Start") -#define TEXT_HOTKEY_STOP obs_module_text("Hotkey.Stop") - -#define TEXT_USE_INDIRECT_HOOK obs_module_text("UseIndirectHook") - -#define TEXT_HOOK_RATE obs_module_text("HookRate") -#define TEXT_HOOK_RATE_SLOW obs_module_text("HookRate.Slow") -#define TEXT_HOOK_RATE_NORMAL obs_module_text("HookRate.Normal") -#define TEXT_HOOK_RATE_FAST obs_module_text("HookRate.Fast") -#define TEXT_HOOK_RATE_FASTEST obs_module_text("HookRate.Fastest") - -#define HOTKEY_START "hotkey_start" -#define HOTKEY_STOP "hotkey_stop" - -#define HOOK_INTERVAL_IMMEDIATE 0.0f -#define HOOK_INTERVAL_PING 0.2f -#define HOOK_INTERVAL_DEFAULT 2.0f -#define HOOK_INTERVAL_ERROR 4.0f -#define HOOK_INTERVAL_KEEPALIVE 4.0f - -/* clang-format on */ - -enum mode { MODE_WINDOW, MODE_HOTKEY }; - -enum hook_rate { - HOOK_RATE_SLOW, - HOOK_RATE_NORMAL, - HOOK_RATE_FAST, - HOOK_RATE_FASTEST -}; - -typedef struct window_info { - char *title; - char *class; - char *executable; -} window_info_t; - -typedef struct audio_capture_config { - enum mode mode; - HWND hotkey_window; - - window_info_t window_info; - enum window_priority priority; - - float retry_interval; - bool use_indirect_hook; -} audio_capture_config_t; - -typedef struct audio_capture_context { - bool worker_initialized; - HANDLE worker_thread; - - CRITICAL_SECTION config_section; - audio_capture_config_t config; - - obs_hotkey_pair_id hotkey_pair; - obs_source_t *source; - - CRITICAL_SECTION timer_section; - HANDLE timer; - HANDLE timer_queue; - - HANDLE hook_data_map; - volatile audio_hook_data_t *hook_data; - - HANDLE events[NUM_EVENTS_TOTAL]; - - DWORD process_id; - DWORD thread_id; - - DWORD next_process_id; - DWORD next_thread_id; - - bool window_selected; - - HANDLE injector_process; - HANDLE target_process; - - bool injected; - bool active; - bool target_opened; - bool target_is_64bit; - - bool use_indirect_hook; -} audio_capture_context_t; - -static inline float hook_rate_to_float(enum hook_rate rate) -{ - switch (rate) { - case HOOK_RATE_SLOW: - return 2.0f; - case HOOK_RATE_FAST: - return 0.5f; - case HOOK_RATE_FASTEST: - return 0.1f; - case HOOK_RATE_NORMAL: - /* FALLTHROUGH */ - default: - return 1.0f; - } -} - -static void window_info_destroy(window_info_t *w) -{ - bfree(w->title); - bfree(w->class); - bfree(w->executable); -} - -static bool window_info_cmp(window_info_t *wa, window_info_t *wb) -{ - if (wa->title == NULL && wb->title == NULL) - return false; - else if (wa->title == NULL || wb->title == NULL) - return true; - - return strcmp(wa->title, wb->title) || strcmp(wa->class, wb->class) || - strcmp(wa->executable, wb->executable); -} - -static HWND window_info_get_window(window_info_t *w, - enum window_priority priority) -{ - HWND window = NULL; - - if (strcmp(w->class, "dwm") == 0) { - wchar_t class_w[512]; - os_utf8_to_wcs(w->class, 0, class_w, 512); - window = FindWindowW(class_w, NULL); - } else { - window = find_window(INCLUDE_MINIMIZED, priority, w->class, - w->title, w->executable); - } - - return window; -} - -VOID CALLBACK set_rehook(PVOID lpParam, BOOLEAN TimerOrWaitFired) +VOID CALLBACK set_update(PVOID lpParam, BOOLEAN TimerOrWaitFired) { audio_capture_context_t *ctx = lpParam; - SetEvent(ctx->events[EVENT_REHOOK]); + SetEvent(ctx->events[EVENT_UPDATE]); } -void set_rehook_timer(audio_capture_context_t *ctx, float interval) +void set_update_timer(audio_capture_context_t *ctx, float interval) { EnterCriticalSection(&ctx->timer_section); @@ -229,152 +42,12 @@ void set_rehook_timer(audio_capture_context_t *ctx, float interval) } debug("setting timer for %ld millis", time_millis); - CreateTimerQueueTimer(&ctx->timer, ctx->timer_queue, set_rehook, ctx, + CreateTimerQueueTimer(&ctx->timer, ctx->timer_queue, set_update, ctx, time_millis, 0, WT_EXECUTEINTIMERTHREAD); LeaveCriticalSection(&ctx->timer_section); } -static wchar_t *get_string_error(DWORD err) -{ - static wchar_t buf[256]; - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - buf, (sizeof(buf) / sizeof(wchar_t)), NULL); - - return buf; -} - -static bool init_hook_data(audio_capture_context_t *ctx) -{ - wchar_t name[MAX_PATH]; - format_name_pid(name, HOOK_DATA_NAME, ctx->process_id); - - ctx->hook_data_map = - CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, - 0, sizeof(audio_hook_data_t), name); - - if (!ctx->hook_data_map) { - error("failed to create file mapping with name: %ls: %ls", name, - get_string_error(GetLastError())); - return false; - } - - ctx->hook_data = MapViewOfFile(ctx->hook_data_map, FILE_MAP_ALL_ACCESS, - 0, 0, sizeof(audio_hook_data_t)); - - if (!ctx->hook_data) { - CloseHandle(ctx->hook_data_map); - ctx->hook_data_map = NULL; - - error("failed to create file map view (%s)", - get_string_error(GetLastError())); - - return false; - } - - return true; -} - -static void destroy_hook_data(audio_capture_context_t *ctx) -{ - if (ctx->hook_data != NULL) { - UnmapViewOfFile((void *)ctx->hook_data); - ctx->hook_data = NULL; - } - - if (ctx->hook_data_map != NULL) { - CloseHandle(ctx->hook_data_map); - ctx->hook_data_map = NULL; - } -} - -static bool init_hook_events(audio_capture_context_t *ctx) -{ - for (int i = HOOK_WO_EVENTS_START; i < HOOK_EVENTS_END; ++i) { - wchar_t name[MAX_PATH]; - format_name_pid(name, event_info[i].name, ctx->process_id); - - ctx->events[i] = - CreateEventW(NULL, event_info[i].reset, FALSE, name); - - if (ctx->events[i] == NULL) - return false; - } - - return true; -} - -static void destroy_hook_events(audio_capture_context_t *ctx) -{ - for (int i = HOOK_WO_EVENTS_START; i < HOOK_EVENTS_END; ++i) { - if (ctx->events[i] != NULL) - CloseHandle(ctx->events[i]); - - ctx->events[i] = NULL; - } -} - -static inline HANDLE open_process(DWORD desired_access, bool inherit_handle, - DWORD process_id) -{ - typedef HANDLE(WINAPI * PFN_OpenProcess)(DWORD, BOOL, DWORD); - - static HMODULE kernel32_handle = NULL; - static PFN_OpenProcess open_process_proc = NULL; - - if (!kernel32_handle) - kernel32_handle = GetModuleHandleW(L"kernel32"); - - if (!open_process_proc) - open_process_proc = (PFN_OpenProcess)get_obfuscated_func( - kernel32_handle, "NuagUykjcxr", 0x1B694B59451ULL); - - return open_process_proc(desired_access, inherit_handle, process_id); -} - -static inline bool is_64bit_windows(void) -{ -#ifdef _WIN64 - return true; -#else - BOOL x86 = false; - bool success = !!IsWow64Process(GetCurrentProcess(), &x86); - return success && !!x86; -#endif -} - -static inline bool is_64bit_process(HANDLE process) -{ - BOOL x86 = true; - if (is_64bit_windows()) { - bool success = !!IsWow64Process(process, &x86); - if (!success) { - return false; - } - } - - return !x86; -} - -static bool open_target_process(audio_capture_context_t *ctx) -{ - ctx->target_process = - open_process(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, false, - ctx->process_id); - - if (!ctx->target_process) { - warn("could not open process: %lu", ctx->process_id); - return false; - } - - ctx->target_opened = true; - ctx->target_is_64bit = is_64bit_process(ctx->target_process); - - return true; -} - #define STOP_BEING_BAD \ " This is most likely due to security software. Please make sure " \ "that the OBS installation folder is excluded/ignored in the " \ @@ -421,505 +94,313 @@ static bool check_file_integrity(audio_capture_context_t *ctx, const char *file, return false; } -static inline int inject_library(HANDLE process, const wchar_t *dll) -{ - return inject_library_obf(process, dll, "D|hkqkW`kl{k\\osofj", - 0xa178ef3655e5ade7, "[uawaRzbhh{tIdkj~~", - 0x561478dbd824387c, "[fr}pboIe`dlN}", - 0x395bfbc9833590fd, "\\`zs}gmOzhhBq", - 0x12897dd89168789a, "GbfkDaezbp~X", - 0x76aff7238788f7db); -} - -static inline bool hook_direct(audio_capture_context_t *ctx, - const char *hook_path_rel) +static inline HANDLE open_process(DWORD desired_access, bool inherit_handle, + DWORD process_id) { - wchar_t hook_path_abs_w[MAX_PATH]; - wchar_t *hook_path_rel_w; - - os_utf8_to_wcs_ptr(hook_path_rel, 0, &hook_path_rel_w); - if (!hook_path_rel_w) { - warn("could not convert string"); - return false; - } - - wchar_t *path_ret = - _wfullpath(hook_path_abs_w, hook_path_rel_w, MAX_PATH); - bfree(hook_path_rel_w); - - if (path_ret == NULL) { - warn("could not make absolute path"); - return false; - } - - debug("made absolute hook path: %ls", hook_path_abs_w); + typedef HANDLE(WINAPI * PFN_OpenProcess)(DWORD, BOOL, DWORD); - HANDLE process = - open_process(PROCESS_ALL_ACCESS, false, ctx->process_id); - if (!process) { - warn("could not open process: %s (%lu)", - ctx->config.window_info.executable, GetLastError()); - return false; - } + static HMODULE kernel32_handle = NULL; + static PFN_OpenProcess open_process_proc = NULL; - int ret = inject_library(process, hook_path_abs_w); - CloseHandle(process); + if (!kernel32_handle) + kernel32_handle = GetModuleHandleW(L"kernel32"); - if (ret != 0) { - warn("inject failed: %d", ret); - return false; - } + if (!open_process_proc) + open_process_proc = (PFN_OpenProcess)get_obfuscated_func( + kernel32_handle, "NuagUykjcxr", 0x1B694B59451ULL); - return true; + return open_process_proc(desired_access, inherit_handle, process_id); } -static inline bool create_inject_process(audio_capture_context_t *ctx, - const char *inject_path, - const char *hook_dll) +static void start_capture(audio_capture_context_t *ctx) { - wchar_t *command_line_w = bzalloc(4096 * sizeof(wchar_t)); + char *helper_path = obs_module_file("audio-capture-helper.exe"); + if (!check_file_integrity(ctx, helper_path, "helper")) + return; - wchar_t *inject_path_w; - wchar_t *hook_dll_w; + wchar_t *helper_path_w; + os_utf8_to_wcs_ptr(helper_path, 0, &helper_path_w); - bool indirect_hook = ctx->use_indirect_hook; + wchar_t *command_line_w = bzalloc(4096 * sizeof(wchar_t)); + swprintf(command_line_w, 4096, L"\"%s\" %lu %S %S", helper_path_w, + ctx->process_id, + ctx->include_process_tree ? "include" : "exclude", ctx->tag); - PROCESS_INFORMATION process_info = {0}; STARTUPINFOW startup_info = {0}; - bool success = false; - - os_utf8_to_wcs_ptr(inject_path, 0, &inject_path_w); - os_utf8_to_wcs_ptr(hook_dll, 0, &hook_dll_w); + PROCESS_INFORMATION process_info = {0}; startup_info.cb = sizeof(startup_info); - swprintf(command_line_w, 4096, L"\"%s\" \"%s\" %lu %lu", inject_path_w, - hook_dll_w, (unsigned long)indirect_hook, - indirect_hook ? ctx->thread_id : ctx->process_id); - - debug("attempting to create helper process with args: \"%ls\"", - command_line_w); - - success = !!CreateProcessW(inject_path_w, command_line_w, NULL, NULL, - false, CREATE_NO_WINDOW, NULL, NULL, - &startup_info, &process_info); + debug("launching helper with command line: %ls", command_line_w); + bool success = CreateProcessW(NULL, command_line_w, NULL, NULL, false, + CREATE_NO_WINDOW, NULL, NULL, + &startup_info, &process_info); if (success) { - CloseHandle(process_info.hThread); + safe_close_handle(&process_info.hThread); - if (ctx->injector_process) - CloseHandle(ctx->injector_process); - - ctx->injector_process = process_info.hProcess; - - debug("created injector process: %llu", ctx->injector_process); + ctx->helper_process_id = process_info.dwProcessId; + ctx->helper_process = process_info.hProcess; } else { - warn("failed to create inject helper process: %lu", - GetLastError()); + error("failed to create helper process"); } - bfree(command_line_w); + ctx->process = open_process(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, + false, ctx->process_id); - bfree(inject_path_w); - bfree(hook_dll_w); + if (ctx->process == NULL) + warn("failed to open target process, can't detect termination"); - return success; + bfree(command_line_w); + bfree(helper_path_w); } -static bool try_inject(audio_capture_context_t *ctx) +static void stop_capture(audio_capture_context_t *ctx) { - debug("attempting to inject: process_id = %lu", ctx->process_id); - - bool success = false; - - char *inject_path; - char *hook_path; - - if (ctx->target_is_64bit) { - debug("target is 64 bit"); - - inject_path = obs_module_file("inject-helper64.exe"); - hook_path = obs_module_file("audio-hook64.dll"); - } else { - debug("target is 32 bit"); - - inject_path = obs_module_file("inject-helper32.exe"); - hook_path = obs_module_file("audio-hook32.dll"); - } - - debug("inject helper path: \"%s\", hook path: \"%s\"", inject_path, - hook_path); - - if (!check_file_integrity(ctx, inject_path, "inject helper")) - goto exit; - - if (!check_file_integrity(ctx, hook_path, "graphics hook")) - goto exit; + if (ctx->helper_process_id != 0) { + SetEvent(ctx->events[HELPER_WO_EVENT_SHUTDOWN]); + WaitForSingleObject(ctx->helper_process, INFINITE); -#ifdef _WIN64 - bool matching_architecture = ctx->target_is_64bit; -#else - bool matching_architecture = !ctx->target_is_64bit; -#endif - - if (matching_architecture && !ctx->use_indirect_hook) { - debug("attempting direct hook"); - success = hook_direct(ctx, hook_path); - } else { - debug("attempting %s helper hook", - ctx->use_indirect_hook ? "indirect" : "direct"); - success = create_inject_process(ctx, inject_path, hook_path); + ResetEvent(ctx->events[HELPER_WO_EVENT_SHUTDOWN]); } -exit: - bfree(inject_path); - bfree(hook_path); - - return success; + ctx->helper_process_id = 0; + safe_close_handle(&ctx->helper_process); + safe_close_handle(&ctx->process); } -static void try_unhook(audio_capture_context_t *ctx) +static void audio_capture_worker_recapture(audio_capture_context_t *ctx) { - if (ctx->target_process != NULL) { - CloseHandle(ctx->target_process); - ctx->target_process = NULL; - } - - if (ctx->injector_process != NULL) { - CloseHandle(ctx->injector_process); - ctx->injector_process = NULL; - } - - if (ctx->events[HOOK_WO_EVENT_STOP] != NULL) { - debug("signalling hook stop"); - SetEvent(ctx->events[HOOK_WO_EVENT_STOP]); - } - - destroy_hook_data(ctx); - destroy_hook_events(ctx); - - ctx->process_id = 0; + stop_capture(ctx); + ctx->process_id = ctx->next_process_id; - ctx->injected = false; - ctx->active = false; - - ctx->target_opened = false; + if (ctx->process_id != 0) + start_capture(ctx); + else if (ctx->window_selected) + set_update_timer(ctx, RECAPTURE_INTERVAL_DEFAULT); } -static void audio_capture_hook_update(audio_capture_context_t *ctx) +static void audio_capture_worker_update(audio_capture_context_t *ctx) { EnterCriticalSection(&ctx->config_section); - debug("mode = %d, priority = %d, retry_interval = %f", - ctx->config.mode, ctx->config.priority, - ctx->config.retry_interval); - - ctx->use_indirect_hook = ctx->config.use_indirect_hook; + ctx->include_process_tree = ctx->config.include_process_tree; if (ctx->config.mode == MODE_HOTKEY) { - debug("hotkey settings: hotkey_window = %lld", - ctx->config.hotkey_window); - if (ctx->config.hotkey_window != NULL) { ctx->window_selected = true; - ctx->next_thread_id = GetWindowThreadProcessId( - ctx->config.hotkey_window, - &ctx->next_process_id); + GetWindowThreadProcessId(ctx->config.hotkey_window, + &ctx->next_process_id); } else { ctx->window_selected = false; ctx->next_process_id = 0; } - LeaveCriticalSection(&ctx->config_section); - return; + goto exit; } if (ctx->config.window_info.title == NULL) { - debug("window settings: no window"); ctx->next_process_id = 0; ctx->window_selected = false; - LeaveCriticalSection(&ctx->config_section); - return; + goto exit; } - debug("window settings: title = %s, class = %s, executable = %s", - ctx->config.window_info.title, ctx->config.window_info.class, - ctx->config.window_info.executable); - ctx->window_selected = true; HWND window = window_info_get_window(&ctx->config.window_info, ctx->config.priority); - if (window != NULL) { - ctx->next_thread_id = - GetWindowThreadProcessId(window, &ctx->next_process_id); - } else { + if (window != NULL) + GetWindowThreadProcessId(window, &ctx->next_process_id); + else ctx->next_process_id = 0; - } +exit: LeaveCriticalSection(&ctx->config_section); + audio_capture_worker_recapture(ctx); } -bool audio_capture_hook_rehook(audio_capture_context_t *ctx) +static void audio_capture_worker_forward(audio_capture_context_t *ctx) { - debug("rehook triggered: process_id = %lu, next_process_id = %lu", - ctx->process_id, ctx->next_process_id); + static uint8_t data[HELPER_DATA_SIZE]; - if (ctx->next_process_id == 0) { - try_unhook(ctx); + if (InterlockedCompareExchange(&ctx->data->lock, 1, 0) != 0) { + warn("failed to acquire data lock, dropping"); + return; + } - if (!ctx->window_selected) - return true; + for (int i = 0; i < ctx->data->data_size; ++i) + data[i] = ctx->data->data[i]; - audio_capture_hook_update(ctx); + struct obs_source_audio audio = { + .data[0] = data, + .frames = ctx->data->audio.frames, - if (ctx->next_process_id == 0) { - set_rehook_timer(ctx, HOOK_INTERVAL_DEFAULT); - return true; - } - } + .speakers = ctx->data->audio.speakers, + .format = ctx->data->audio.format, + .samples_per_sec = ctx->data->audio.samples_per_sec, - if (ctx->injected && ctx->process_id == ctx->next_process_id) { - if (!ctx->active) { - SetEvent(ctx->events[HOOK_WO_EVENT_START]); - set_rehook_timer(ctx, HOOK_INTERVAL_DEFAULT); - return true; - } + .timestamp = ctx->data->audio.timestamp}; - DWORD exit_code = 0; - if (!GetExitCodeProcess(ctx->target_process, &exit_code)) { - warn("failed to get target exit code (%s)", - get_string_error(GetLastError())); - } + InterlockedExchange(&ctx->data->lock, 0); - if (exit_code != STILL_ACTIVE) { - debug("target process died, rehooking"); - ctx->next_process_id = 0; - try_unhook(ctx); - - set_rehook_timer(ctx, HOOK_INTERVAL_IMMEDIATE); - return true; - } + obs_source_output_audio(ctx->source, &audio); +} - set_rehook_timer(ctx, HOOK_INTERVAL_KEEPALIVE); - return true; - } +static bool audio_capture_worker_tick(audio_capture_context_t *ctx, + int event_id) +{ + bool shutdown = false; - // TODO: causes double-update on settings change... - // shouldn't matter (and better than the alternative) - // but a bit wasteful - audio_capture_hook_update(ctx); + switch (event_id) { + case HELPER_EVENT_DATA: + audio_capture_worker_forward(ctx); + break; - if (ctx->process_id != ctx->next_process_id) { - try_unhook(ctx); + case EVENT_SHUTDOWN: + debug("shutting down"); - ctx->process_id = ctx->next_process_id; - ctx->thread_id = ctx->next_thread_id; + stop_capture(ctx); + shutdown = true; - if (!init_hook_data(ctx)) { - error("failed to create hook data"); - return false; - } + break; - if (!init_hook_events(ctx)) { - error("failed to create hook events"); - return false; - } + case EVENT_UPDATE: + audio_capture_worker_update(ctx); + break; - if (!open_target_process(ctx)) { - warn("failed to open target process"); - set_rehook_timer(ctx, HOOK_INTERVAL_ERROR); + case EVENT_PROCESS_TARGET: + debug("target process died"); - return true; - } + safe_close_handle(&ctx->process); + ctx->process_id = 0; - SetEvent(ctx->events[HOOK_WO_EVENT_PING]); - set_rehook_timer(ctx, HOOK_INTERVAL_PING); + audio_capture_worker_update(ctx); + break; - return true; - } + case EVENT_PROCESS_HELPER: + DWORD code; + bool success = GetExitCodeProcess(ctx->helper_process, &code); + if (success) + warn("helper died with exit code: %lu", code); + else + warn("helper died and failed to get exit code"); - if (ctx->injector_process && - WaitForSingleObject(ctx->injector_process, 0) == WAIT_OBJECT_0) { - DWORD exit_code = 0; - GetExitCodeProcess(ctx->injector_process, &exit_code); + set_update_timer(ctx, RECAPTURE_INTERVAL_ERROR); + break; - if (exit_code != 0) { - warn("last inject process failed: %ld", - (long)exit_code); - } else { - debug("last inject process succeeded!"); - } - } + default: + error("unexpected event id"); - if (!ctx->target_opened && !open_target_process(ctx)) { - set_rehook_timer(ctx, HOOK_INTERVAL_ERROR); - return true; - } + stop_capture(ctx); + shutdown = true; - if (!ctx->injected && !try_inject(ctx)) { - warn("try_inject failed"); - set_rehook_timer(ctx, HOOK_INTERVAL_ERROR); - return true; + break; } - SetEvent(ctx->events[HOOK_WO_EVENT_START]); - set_rehook_timer(ctx, HOOK_INTERVAL_DEFAULT); - - return true; + return shutdown; } -static enum speaker_layout get_obs_speaker_layout(WAVEFORMATEXTENSIBLE *format) +static void destroy_data(audio_capture_context_t *ctx) { - switch (format->Format.nChannels) { - case 1: - return SPEAKERS_MONO; - case 2: - return SPEAKERS_STEREO; - case 3: - return SPEAKERS_2POINT1; - case 4: - return SPEAKERS_4POINT0; - case 5: - return SPEAKERS_4POINT1; - case 6: - return SPEAKERS_5POINT1; - case 8: - return SPEAKERS_7POINT1; + if (ctx->data != NULL) { + UnmapViewOfFile((void **)ctx->data); + ctx->data = NULL; } - return SPEAKERS_UNKNOWN; + safe_close_handle(&ctx->data_map); } -static enum audio_format get_obs_pcm_format(int bits_per_sample) +static bool init_data(audio_capture_context_t *ctx) { - switch (bits_per_sample) { - case 8: - return AUDIO_FORMAT_U8BIT; - case 16: - return AUDIO_FORMAT_16BIT; - case 32: - return AUDIO_FORMAT_32BIT; - }; - return AUDIO_FORMAT_UNKNOWN; -} + wchar_t name[MAX_PATH]; + format_name_tag(name, HELPER_DATA_NAME, ctx->tag); + ctx->data_map = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, + PAGE_READWRITE, 0, + sizeof(audio_capture_helper_data_t), + name); + if (ctx->data_map == NULL) { + error("failed to create file mapping with name: %ls", name); + return false; + } -static enum audio_format get_obs_format(WAVEFORMATEXTENSIBLE *format) -{ - switch (format->Format.wFormatTag) { - case WAVE_FORMAT_PCM: - return get_obs_pcm_format(format->Format.wBitsPerSample); - - case WAVE_FORMAT_IEEE_FLOAT: - return AUDIO_FORMAT_FLOAT; - - case WAVE_FORMAT_EXTENSIBLE: - if (IsEqualGUID(&format->SubFormat, - &KSDATAFORMAT_SUBTYPE_PCM)) { - return get_obs_pcm_format( - format->Format.wBitsPerSample); - } else if (IsEqualGUID(&format->SubFormat, - &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) { - return AUDIO_FORMAT_FLOAT; - } + ctx->data = MapViewOfFile(ctx->data_map, FILE_MAP_ALL_ACCESS, 0, 0, + sizeof(audio_capture_helper_data_t)); + if (ctx->data == NULL) { + error("failed to map view of data map"); + return false; } - return AUDIO_FORMAT_UNKNOWN; + return true; } -static void audio_capture_hook_forward_data(audio_capture_context_t *ctx) +static void destroy_helper_events(audio_capture_context_t *ctx) { - if (InterlockedCompareExchange(&ctx->hook_data->lock, 1, 0) != 0) { - warn("couldn't acquire lock"); - return; - } - - struct obs_source_audio audio; - - audio.data[0] = (uint8_t *)ctx->hook_data->data; - audio.frames = ctx->hook_data->frames; - - audio.speakers = get_obs_speaker_layout( - (WAVEFORMATEXTENSIBLE *)&ctx->hook_data->format); - audio.format = - get_obs_format((WAVEFORMATEXTENSIBLE *)&ctx->hook_data->format); - audio.samples_per_sec = ctx->hook_data->format.Format.nSamplesPerSec; - - audio.timestamp = ctx->hook_data->timestamp; - - obs_source_output_audio(ctx->source, &audio); - - InterlockedExchange(&ctx->hook_data->lock, 0); + for (int i = HELPER_WO_EVENTS_START; i < HELPER_EVENTS_END; ++i) + safe_close_handle(&ctx->events[i]); } -static bool audio_capture_tick(audio_capture_context_t *ctx, int event_id) +static bool init_helper_events(audio_capture_context_t *ctx) { - bool shutdown = false; - - switch (event_id) { - case EVENT_SHUTDOWN: - debug("shutting down"); - - try_unhook(ctx); - shutdown = true; - - break; - - case EVENT_REHOOK: - shutdown = !audio_capture_hook_rehook(ctx); - break; - - case EVENT_UPDATE: - audio_capture_hook_update(ctx); - shutdown = !audio_capture_hook_rehook(ctx); - break; - - case HOOK_EVENT_READY: - debug("hook ready"); - ctx->injected = true; - - break; - case HOOK_EVENT_ACTIVE: - debug("hook activated"); - ctx->active = true; + for (int i = HELPER_WO_EVENTS_START; i < HELPER_EVENTS_END; ++i) { + wchar_t name[MAX_PATH]; + format_name_tag(name, event_names[i], ctx->tag); - break; + ctx->events[i] = CreateEventW(NULL, FALSE, FALSE, name); + if (ctx->events[i] == NULL) { + error("failed to create helper event"); + return false; + } + } - case HOOK_EVENT_DATA: - audio_capture_hook_forward_data(ctx); + return true; +} - break; +static DWORD WINAPI audio_capture_worker_thread(LPVOID lpParam) +{ + audio_capture_context_t *ctx = lpParam; + int ret = 0; - default: - error("unexpected event id"); + ctx->tag = bzalloc(MAX_PATH * sizeof(char)); + format_tag(ctx->tag); - try_unhook(ctx); - shutdown = true; + debug("tag is: %s", ctx->tag); - break; + if (!init_helper_events(ctx)) { + error("failed to init helper events"); + ret = 1; + goto exit; } - return shutdown; -} + if (!init_data(ctx)) { + error("failed to init shmem data"); + ret = 1; + goto exit; + } -static DWORD WINAPI audio_capture_thread(LPVOID lpParam) -{ - audio_capture_context_t *ctx = lpParam; + int num_perm_events = NUM_HELPER_EVENTS + NUM_EVENTS; + HANDLE *events = bzalloc((2 + num_perm_events) * sizeof(HANDLE)); + for (int i = 0; i < num_perm_events; ++i) + events[i] = ctx->events[HELPER_EVENTS_START + i]; bool shutdown = false; while (!shutdown) { - int num_events = NUM_EVENTS; - int start_event = EVENTS_START; + int num_proc_events = 0; + if (ctx->process != NULL) { + events[num_perm_events + num_proc_events] = + ctx->process; + num_proc_events++; + } - if (ctx->events[HOOK_EVENTS_START] != NULL) { - num_events += NUM_HOOK_EVENTS; - start_event = HOOK_EVENTS_START; + if (ctx->helper_process != NULL) { + events[num_perm_events + num_proc_events] = + ctx->helper_process; + num_proc_events++; } - HANDLE *events = &ctx->events[start_event]; + int num_events = num_perm_events + num_proc_events; DWORD event_id = WaitForMultipleObjects(num_events, events, FALSE, INFINITE); @@ -929,11 +410,20 @@ static DWORD WINAPI audio_capture_thread(LPVOID lpParam) return 1; } - event_id -= WAIT_OBJECT_0; - shutdown = audio_capture_tick(ctx, start_event + event_id); + event_id += HELPER_EVENTS_START - WAIT_OBJECT_0; + + // TODO make this less awkward? + if (num_proc_events == 1 && event_id == EVENT_PROCESS_TARGET && + ctx->process == NULL) + event_id = EVENT_PROCESS_HELPER; + + shutdown = audio_capture_worker_tick(ctx, event_id); } - return 0; +exit: + destroy_helper_events(ctx); + destroy_data(ctx); + return ret; } static void audio_capture_update(void *data, obs_data_t *settings) @@ -941,56 +431,52 @@ static void audio_capture_update(void *data, obs_data_t *settings) audio_capture_context_t *ctx = data; bool need_update = false; - enum mode mode = obs_data_get_int(settings, SETTING_MODE); + audio_capture_config_t new_config = { + .mode = obs_data_get_int(settings, SETTING_MODE), + .priority = obs_data_get_int(settings, SETTING_WINDOW_PRIORITY), + .include_process_tree = obs_data_get_bool( + settings, SETTING_INCLUDE_PROCESS_TREE), + .retry_interval = recapture_rate_to_float( + obs_data_get_int(settings, SETTING_RECAPTURE_RATE)), + }; const char *window = obs_data_get_string(settings, SETTING_WINDOW); - enum window_priority priority = - obs_data_get_int(settings, SETTING_WINDOW_PRIORITY); - - bool use_indirect_hook = - obs_data_get_bool(settings, SETTING_USE_INDIRECT_HOOK); - - enum hook_rate hook_rate = - obs_data_get_int(settings, SETTING_HOOK_RATE); - - float retry_interval = hook_rate_to_float(hook_rate); + build_window_strings(window, &new_config.window_info); EnterCriticalSection(&ctx->config_section); - if (mode == MODE_HOTKEY) { - if (ctx->config.mode != mode) + if (new_config.mode == MODE_HOTKEY) { + if (ctx->config.mode != new_config.mode) ctx->config.hotkey_window = NULL; } else { - window_info_t window_info = {NULL, NULL, NULL}; - build_window_strings(window, &window_info.class, - &window_info.title, - &window_info.executable); - - if (window_info_cmp(&window_info, &ctx->config.window_info)) { - ctx->config.window_info = window_info; + if (window_info_cmp(&new_config.window_info, + &ctx->config.window_info)) { + ctx->config.window_info = new_config.window_info; need_update = true; } else { - window_info_destroy(&window_info); + window_info_destroy(&new_config.window_info); } - if (ctx->config.priority != priority) { - ctx->config.priority = priority; + if (ctx->config.priority != new_config.priority) { + ctx->config.priority = new_config.priority; need_update = true; } } - if (ctx->config.mode != mode) { - ctx->config.mode = mode; + if (ctx->config.mode != new_config.mode) { + ctx->config.mode = new_config.mode; need_update = true; } - if (ctx->config.use_indirect_hook != use_indirect_hook) { - ctx->config.use_indirect_hook = use_indirect_hook; + if (ctx->config.include_process_tree != + new_config.include_process_tree) { + ctx->config.include_process_tree = + new_config.include_process_tree; need_update = true; } - if (ctx->config.retry_interval != retry_interval) { - ctx->config.retry_interval = retry_interval; + if (ctx->config.retry_interval != new_config.retry_interval) { + ctx->config.retry_interval = new_config.retry_interval; need_update = true; } @@ -1018,7 +504,7 @@ static bool hotkey_start(void *data, obs_hotkey_pair_id id, LeaveCriticalSection(&ctx->config_section); if (needs_update) - set_rehook_timer(ctx, HOOK_INTERVAL_IMMEDIATE); + SetEvent(ctx->events[EVENT_UPDATE]); return true; } @@ -1040,7 +526,7 @@ static bool hotkey_stop(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, LeaveCriticalSection(&ctx->config_section); if (needs_update) - set_rehook_timer(ctx, HOOK_INTERVAL_IMMEDIATE); + SetEvent(ctx->events[EVENT_UPDATE]); return true; } @@ -1048,29 +534,24 @@ static bool hotkey_stop(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, static void audio_capture_destroy(void *data) { audio_capture_context_t *ctx = data; - if (!ctx) + if (ctx == NULL) return; - try_unhook(ctx); + if (ctx->worker_initialized) { + SetEvent(ctx->events[EVENT_SHUTDOWN]); + WaitForSingleObject(ctx->worker_thread, INFINITE); + } + safe_close_handle(&ctx->worker_thread); + if (ctx->timer != NULL) DeleteTimerQueueTimer(ctx->timer_queue, ctx->timer, NULL); if (ctx->timer_queue != NULL) DeleteTimerQueue(ctx->timer_queue); - if (ctx->worker_initialized) { - SetEvent(ctx->events[EVENT_SHUTDOWN]); - WaitForSingleObject(ctx->worker_thread, INFINITE); - } - - if (ctx->worker_thread != NULL) - CloseHandle(ctx->worker_thread); - - for (int i = EVENTS_START; i < EVENTS_END; ++i) { - if (ctx->events[i] != NULL) - CloseHandle(ctx->events[i]); - } + for (int i = HELPER_WO_EVENTS_START; i < EVENTS_END; ++i) + safe_close_handle(&ctx->events[i]); if (ctx->hotkey_pair) obs_hotkey_pair_unregister(ctx->hotkey_pair); @@ -1080,6 +561,7 @@ static void audio_capture_destroy(void *data) DeleteCriticalSection(&ctx->config_section); DeleteCriticalSection(&ctx->timer_section); + bfree(ctx->tag); bfree(ctx); } @@ -1094,14 +576,17 @@ static void *audio_capture_create(obs_data_t *settings, obs_source_t *source) InitializeCriticalSection(&ctx->timer_section); ctx->timer_queue = CreateTimerQueue(); - if (ctx->timer_queue == NULL) + if (ctx->timer_queue == NULL) { + error("failed to create timer queue"); goto fail; + } for (int i = EVENTS_START; i < EVENTS_END; ++i) { - ctx->events[i] = CreateEventW(NULL, event_info[i].reset, FALSE, - event_info[i].name); - if (ctx->events[i] == NULL) + ctx->events[i] = CreateEventW(NULL, FALSE, FALSE, NULL); + if (ctx->events[i] == NULL) { + error("failed to create event"); goto fail; + } } ctx->hotkey_pair = obs_hotkey_pair_register_source( @@ -1110,13 +595,14 @@ static void *audio_capture_create(obs_data_t *settings, obs_source_t *source) audio_capture_update(ctx, settings); - ctx->worker_thread = - CreateThread(NULL, 0, audio_capture_thread, ctx, 0, NULL); - if (ctx->worker_thread == NULL) + ctx->worker_thread = CreateThread(NULL, 0, audio_capture_worker_thread, + ctx, 0, NULL); + if (ctx->worker_thread == NULL) { + error("failed to create worker thread"); goto fail; + } ctx->worker_initialized = true; - return ctx; fail: @@ -1140,17 +626,17 @@ static bool mode_callback(obs_properties_t *ps, obs_property_t *p, static void insert_preserved_val(obs_property_t *p, const char *val, size_t idx) { - window_info_t w = {NULL, NULL, NULL}; + window_info_t info = {NULL, NULL, NULL}; struct dstr desc = {0}; - build_window_strings(val, &w.class, &w.title, &w.executable); + build_window_strings(val, &info); - dstr_printf(&desc, "[%s]: %s", w.executable, w.title); + dstr_printf(&desc, "[%s]: %s", info.executable, info.title); obs_property_list_insert_string(p, idx, desc.array, val); obs_property_list_item_disable(p, idx, true); dstr_free(&desc); - window_info_destroy(&w); + window_info_destroy(&info); } static bool check_window_property_setting(obs_properties_t *ps, @@ -1242,18 +728,23 @@ static obs_properties_t *audio_capture_properties(void *data) obs_property_list_add_int(p, TEXT_WINDOW_PRIORITY_EXE, WINDOW_PRIORITY_EXE); - // Anti-cheat compatibility hook setting - p = obs_properties_add_bool(ps, SETTING_USE_INDIRECT_HOOK, - TEXT_USE_INDIRECT_HOOK); + // Include process tree setting + p = obs_properties_add_bool(ps, SETTING_INCLUDE_PROCESS_TREE, + TEXT_INCLUDE_PROCESS_TREE); - // Hook rate setting - p = obs_properties_add_list(ps, SETTING_HOOK_RATE, TEXT_HOOK_RATE, - OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + // Recapture rate setting + p = obs_properties_add_list(ps, SETTING_RECAPTURE_RATE, + TEXT_RECAPTURE_RATE, OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); - obs_property_list_add_int(p, TEXT_HOOK_RATE_SLOW, HOOK_RATE_SLOW); - obs_property_list_add_int(p, TEXT_HOOK_RATE_NORMAL, HOOK_RATE_NORMAL); - obs_property_list_add_int(p, TEXT_HOOK_RATE_FAST, HOOK_RATE_FAST); - obs_property_list_add_int(p, TEXT_HOOK_RATE_FASTEST, HOOK_RATE_FASTEST); + obs_property_list_add_int(p, TEXT_RECAPTURE_RATE_SLOW, + RECAPTURE_RATE_SLOW); + obs_property_list_add_int(p, TEXT_RECAPTURE_RATE_NORMAL, + RECAPTURE_RATE_NORMAL); + obs_property_list_add_int(p, TEXT_RECAPTURE_RATE_FAST, + RECAPTURE_RATE_FAST); + obs_property_list_add_int(p, TEXT_RECAPTURE_RATE_FASTEST, + RECAPTURE_RATE_FASTEST); return ps; } @@ -1264,8 +755,9 @@ static void audio_capture_defaults(obs_data_t *settings) obs_data_set_default_string(settings, SETTING_WINDOW, ""); obs_data_set_default_int(settings, SETTING_WINDOW_PRIORITY, WINDOW_PRIORITY_EXE); - obs_data_set_default_bool(settings, SETTING_USE_INDIRECT_HOOK, true); - obs_data_set_default_int(settings, SETTING_HOOK_RATE, HOOK_RATE_NORMAL); + obs_data_set_default_bool(settings, SETTING_INCLUDE_PROCESS_TREE, true); + obs_data_set_default_int(settings, SETTING_RECAPTURE_RATE, + RECAPTURE_RATE_NORMAL); } static const char *audio_capture_get_name(void *type_data) diff --git a/src/audio-capture.h b/src/audio-capture.h new file mode 100644 index 0000000..a79ee7c --- /dev/null +++ b/src/audio-capture.h @@ -0,0 +1,148 @@ +#pragma once + +#include +#include + +#include +#include + +#include "common.h" +#include "window-helpers.h" + +#define do_log(level, format, ...) \ + do_log_source(ctx->source, level, "(%s) " format, __func__, \ + ##__VA_ARGS__) + +inline static void do_log_source(const obs_source_t *source, int level, + const char *format, ...) +{ + va_list args; + va_start(args, format); + + const char *name = obs_source_get_name(source); + int len = strlen(name); + + const char *format_source = len <= 8 ? "[audio-capture: '%s'] %s" + : "[audio-capture: '%.8s...'] %s"; + + int len_full = strlen(format_source) + 12 + strlen(format); + char *format_full = bzalloc(len_full); + + snprintf(format_full, len_full, format_source, name, format); + blogva(level, format_full, args); + + bfree(format_full); + va_end(args); +} + +#define error(format, ...) do_log(LOG_ERROR, format, ##__VA_ARGS__) +#define warn(format, ...) do_log(LOG_WARNING, format, ##__VA_ARGS__) +#define info(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__) +#define debug(format, ...) do_log(LOG_DEBUG, format, ##__VA_ARGS__) + +/* clang-format off */ + +#define SETTING_MODE "mode" + +#define SETTING_WINDOW "window" +#define SETTING_WINDOW_PRIORITY "window_priority" + +#define SETTING_INCLUDE_PROCESS_TREE "include_process_tree" + +#define SETTING_RECAPTURE_RATE "recapture_rate" + +#define TEXT_MODE obs_module_text("Mode") +#define TEXT_MODE_WINDOW obs_module_text("Mode.Window") +#define TEXT_MODE_HOTKEY obs_module_text("Mode.Hotkey") + +#define TEXT_WINDOW obs_module_text("Window") +#define TEXT_WINDOW_PRIORITY obs_module_text("Window.Priority") +#define TEXT_WINDOW_PRIORITY_TITLE obs_module_text("Window.Priority.Title") +#define TEXT_WINDOW_PRIORITY_CLASS obs_module_text("Window.Priority.Class") +#define TEXT_WINDOW_PRIORITY_EXE obs_module_text("Window.Priority.Exe") + +#define TEXT_HOTKEY_START obs_module_text("Hotkey.Start") +#define TEXT_HOTKEY_STOP obs_module_text("Hotkey.Stop") + +#define TEXT_INCLUDE_PROCESS_TREE obs_module_text("IncludeProcessTree") + +#define TEXT_RECAPTURE_RATE obs_module_text("RecaptureRate") +#define TEXT_RECAPTURE_RATE_SLOW obs_module_text("RecaptureRate.Slow") +#define TEXT_RECAPTURE_RATE_NORMAL obs_module_text("RecaptureRate.Normal") +#define TEXT_RECAPTURE_RATE_FAST obs_module_text("RecaptureRate.Fast") +#define TEXT_RECAPTURE_RATE_FASTEST obs_module_text("RecaptureRate.Fastest") + +#define HOTKEY_START "hotkey_start" +#define HOTKEY_STOP "hotkey_stop" + +#define RECAPTURE_INTERVAL_DEFAULT 2.0f +#define RECAPTURE_INTERVAL_ERROR 4.0f + +/* clang-format on */ + +enum mode { MODE_WINDOW, MODE_HOTKEY }; + +enum recapture_rate { + RECAPTURE_RATE_SLOW, + RECAPTURE_RATE_NORMAL, + RECAPTURE_RATE_FAST, + RECAPTURE_RATE_FASTEST +}; + +static inline float recapture_rate_to_float(enum recapture_rate rate) +{ + switch (rate) { + case RECAPTURE_RATE_SLOW: + return 2.0f; + case RECAPTURE_RATE_FAST: + return 0.5f; + case RECAPTURE_RATE_FASTEST: + return 0.1f; + case RECAPTURE_RATE_NORMAL: + /* FALLTHROUGH */ + default: + return 1.0f; + } +} + +typedef struct audio_capture_config { + enum mode mode; + HWND hotkey_window; + + window_info_t window_info; + enum window_priority priority; + + bool include_process_tree; + float retry_interval; +} audio_capture_config_t; + +typedef struct audio_capture_context { + bool worker_initialized; + HANDLE worker_thread; + + CRITICAL_SECTION config_section; + audio_capture_config_t config; + + obs_hotkey_pair_id hotkey_pair; + obs_source_t *source; + + CRITICAL_SECTION timer_section; + HANDLE timer; + HANDLE timer_queue; + + char *tag; + HANDLE events[NUM_EVENTS_TOTAL]; + + HANDLE data_map; + volatile audio_capture_helper_data_t *data; + + HANDLE helper_process; + DWORD helper_process_id; + + HANDLE process; + DWORD process_id; + DWORD next_process_id; + + bool window_selected; + bool include_process_tree; +} audio_capture_context_t; \ No newline at end of file diff --git a/src/audio-hook/CMakeLists.txt b/src/audio-hook/CMakeLists.txt deleted file mode 100644 index e515d39..0000000 --- a/src/audio-hook/CMakeLists.txt +++ /dev/null @@ -1,23 +0,0 @@ -project(audio-hook) - -find_package(Detours REQUIRED) - -set(audio-hook_SOURCES - audio-hook.c - ../obfuscate.c) - -add_library(audio-hook MODULE ${audio-hook_SOURCES}) - -target_include_directories(audio-hook PUBLIC ${DETOURS_INCLUDE_DIR}) -target_link_libraries(audio-hook ${DETOURS_LIBRARIES} psapi) - -if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(_output_suffix "64") -else() - set(_output_suffix "32") -endif() - -set_target_properties(audio-hook - PROPERTIES OUTPUT_NAME "audio-hook${_output_suffix}") - -copy_helper_lib(audio-hook) \ No newline at end of file diff --git a/src/audio-hook/audio-hook.c b/src/audio-hook/audio-hook.c deleted file mode 100644 index 2facdd1..0000000 --- a/src/audio-hook/audio-hook.c +++ /dev/null @@ -1,481 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include - -#include "../obfuscate.h" -#include "../hook-info.h" - -#define UNUSED_PARAMETER(param) (void)param - -#define DEBUG_OUTPUT - -#ifdef DEBUG_OUTPUT -#define log(fmt, ...) \ - dbg_log("[OBS (%s:%d %s)] " fmt "\n", __FILE__, __LINE__, __func__, \ - ##__VA_ARGS__) -#else -#define log(fmt, ...) -#endif - -static audio_hook_offsets_t offsets; - -static HANDLE hook_data_map; -static volatile audio_hook_data_t *hook_data; - -static BYTE *data; - -static uintptr_t audioses_module; - -static IAudioClientVtbl *client_vtbl; -static IAudioRenderClientVtbl *render_client_vtbl; - -static HANDLE capture_thread; - -static HANDLE events[NUM_HOOK_EVENTS_TOTAL]; - -static bool hooked = false; - -static void dbg_log(const char *fmt, ...) -{ - va_list argp; - va_start(argp, fmt); - - char buf[256]; - vsprintf_s(buf, 256, fmt, argp); - - OutputDebugStringA(buf); -} - -static bool init_offsets() -{ - HANDLE hook_metadata_map = NULL; - volatile audio_hook_metadata_t *hook_metadata = NULL; - - log("opening metadata map: %ls", HOOK_METADATA_NAME); - hook_metadata_map = - OpenFileMappingW(FILE_MAP_READ, FALSE, HOOK_METADATA_NAME); - - if (!hook_metadata_map) { - log("failed to open file mapping"); - return false; - } - - hook_metadata = MapViewOfFile(hook_metadata_map, FILE_MAP_READ, 0, 0, - sizeof(audio_hook_metadata_t)); - - if (!hook_metadata) { - log("failed to open file map view"); - - CloseHandle(hook_metadata_map); - return false; - } - - int ret = false; - - if (!hook_metadata->initialized) { - log("hook metadata not (yet) initialized"); - goto exit; - } - -#ifdef _WIN64 - log("using 64-bit offsets"); - offsets = hook_metadata->offsets64; -#else - log("using 32-bit offsets"); - offsets = hook_metadata->offsets32; -#endif - - log("loaded offsets: client_vtbl = 0x%x, " - "render_client_vtbl = 0x%x, m_render_client_format = 0x%x", - offsets.wasapi.client_vtbl, offsets.wasapi.render_client_vtbl, - offsets.wasapi.m_render_client_format); - - ret = true; - -exit: - UnmapViewOfFile((void *)hook_metadata); - CloseHandle(hook_metadata_map); - - return ret; -} - -static bool init_data() -{ - wchar_t name[MAX_PATH]; - format_name(name, HOOK_DATA_NAME); - - log("opening data map: %ls", name); - hook_data_map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name); - if (!hook_data_map) { - log("failed to open file mapping"); - return false; - } - - hook_data = MapViewOfFile(hook_data_map, FILE_MAP_ALL_ACCESS, 0, 0, - sizeof(audio_hook_data_t)); - - if (!hook_data) { - log("failed to open file map view"); - - CloseHandle(hook_data_map); - return false; - } - - return true; -} - -static void destroy_data() -{ - if (hook_data != NULL) { - UnmapViewOfFile((void *)hook_data); - hook_data = NULL; - } - - if (hook_data_map != NULL) { - CloseHandle(hook_data_map); - hook_data_map = NULL; - } -} - -static bool init_events() -{ - - for (int i = HOOK_WO_EVENTS_START; i < HOOK_EVENTS_END; ++i) { - wchar_t name[MAX_PATH]; - format_name(name, event_info[i].name); - - log("opening event: %ls", name); - events[i] = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, false, - name); - - if (events[i] == NULL) { - log("failed to open event"); - return false; - } - } - - return true; -} - -static void destroy_events() -{ - for (int i = HOOK_WO_EVENTS_START; i < HOOK_EVENTS_END; ++i) { - if (events[i] != NULL) { - CloseHandle(events[i]); - events[i] = NULL; - } - } -} - -static inline uint64_t get_clockfreq(void) -{ - static bool have_clockfreq = false; - static LARGE_INTEGER clock_freq; - - if (!have_clockfreq) { - QueryPerformanceFrequency(&clock_freq); - have_clockfreq = true; - } - - return clock_freq.QuadPart; -} - -static inline uint64_t get_timestamp(void) -{ - LARGE_INTEGER current_time; - double time_val; - - QueryPerformanceCounter(¤t_time); - time_val = (double)current_time.QuadPart; - time_val *= 1000000000.0; - time_val /= (double)get_clockfreq(); - - return (uint64_t)time_val; -} - -static inline uint64_t util_mul_div64(uint64_t num, uint64_t mul, uint64_t div) -{ - const uint64_t rem = num % div; - - return (num / div) * mul + (rem * mul) / div; -} - -WAVEFORMATEXTENSIBLE * -format_from_render_client(IAudioRenderClient *render_client) -{ - uint32_t offset = offsets.wasapi.m_render_client_format; - return *(WAVEFORMATEXTENSIBLE **)((uintptr_t)render_client + offset); -} - -IAudioClient *client_from_render_client(IAudioRenderClient *render_client) -{ - uint32_t offset = offsets.wasapi.m_render_client_client; - return *(IAudioClient **)((uintptr_t)render_client + offset); -} - -HRESULT(STDMETHODCALLTYPE *RealReleaseBuffer) -(IAudioRenderClient *, UINT32, DWORD) = NULL; - -HRESULT(STDMETHODCALLTYPE *RealGetBuffer) -(IAudioRenderClient *, UINT32, BYTE **) = NULL; - -HRESULT STDMETHODCALLTYPE MyGetBuffer(IAudioRenderClient *This, - UINT32 NumFramesRequested, BYTE **ppData) -{ - HRESULT hr = RealGetBuffer(This, NumFramesRequested, ppData); - data = hr == S_OK ? *ppData : NULL; - - return hr; -} - -HRESULT STDMETHODCALLTYPE MyReleaseBuffer(IAudioRenderClient *This, - UINT32 NumFramesWritten, - DWORD dwFlags) -{ - if (data == NULL || NumFramesWritten == 0) - goto exit; - - if (InterlockedCompareExchange(&hook_data->lock, 1, 0) != 0) - goto exit; - - __try { - WAVEFORMATEXTENSIBLE *format = format_from_render_client(This); - if (format->Format.cbSize >= 22) - hook_data->format = *format; - else - hook_data->format.Format = format->Format; - - } __except (EXCEPTION_EXECUTE_HANDLER) { - log("failed to get buffer format"); - - InterlockedExchange(&hook_data->lock, 0); - goto exit; - } - - size_t size = (hook_data->format.Format.wBitsPerSample / CHAR_BIT) * - hook_data->format.Format.nChannels * NumFramesWritten; - - if (size > HOOK_DATA_SIZE) { - InterlockedExchange(&hook_data->lock, 0); - goto exit; - } - - memcpy((void *)hook_data->data, data, size); - - hook_data->timestamp = get_timestamp(); - hook_data->timestamp -= - util_mul_div64(NumFramesWritten, 1000000000ULL, - hook_data->format.Format.nSamplesPerSec); - - hook_data->frames = NumFramesWritten; - - InterlockedExchange(&hook_data->lock, 0); - SetEvent(events[HOOK_EVENT_DATA]); - -exit: - return RealReleaseBuffer(This, NumFramesWritten, dwFlags); -} - -static void start_capture() -{ - log("starting capture"); - - if (hooked) { - log("already hooked, skipping"); - return; - } - - if (!init_offsets()) { - log("couldn't init offsets from hook metadata"); - return; - } - - audioses_module = (uintptr_t)GetModuleHandleW(L"audioses.dll"); - if (audioses_module == (uintptr_t)NULL) { - log("couldn't find loaded audioses.dll"); - return; - } - - render_client_vtbl = - (IAudioRenderClientVtbl *)(audioses_module + - offsets.wasapi.render_client_vtbl); - client_vtbl = (IAudioClientVtbl *)(audioses_module + - offsets.wasapi.client_vtbl); - - log("audioses_module: %p, render_client_vtbl: %p, client_vtbl: %p", - (LPVOID)audioses_module, render_client_vtbl, client_vtbl); - - RealGetBuffer = render_client_vtbl->GetBuffer; - RealReleaseBuffer = render_client_vtbl->ReleaseBuffer; - - data = NULL; - - DetourTransactionBegin(); - DetourUpdateThread(GetCurrentThread()); - - DetourAttach((void **)&RealGetBuffer, MyGetBuffer); - DetourAttach((void **)&RealReleaseBuffer, MyReleaseBuffer); - - LONG err = DetourTransactionCommit(); - - if (err == NO_ERROR) { - log("hooked successfully"); - - hooked = true; - SetEvent(events[HOOK_EVENT_ACTIVE]); - } else { - log("error while hooking: %ld", err); - } -} - -static void stop_capture() -{ - log("stopping capture"); - - if (!hooked) { - log("not hooked, skipping"); - return; - } - - DetourTransactionBegin(); - DetourUpdateThread(GetCurrentThread()); - - DetourDetach((void **)&RealGetBuffer, MyGetBuffer); - DetourDetach((void **)&RealReleaseBuffer, MyReleaseBuffer); - - LONG err = DetourTransactionCommit(); - - data = NULL; - - if (err == NO_ERROR) { - log("unhooked successfully"); - hooked = false; - } else { - log("error while unhooking: %d", err); - } -} - -static DWORD WINAPI main_capture_thread(LPVOID lpParam) -{ - UNUSED_PARAMETER(lpParam); - - if (!init_data()) { - log("failed to init data"); - goto exit; - } - - if (!init_events()) { - log("failed to init events"); - goto exit; - } - - bool shutdown = false; - while (!shutdown) { - DWORD event_id = WaitForMultipleObjects( - NUM_HOOK_WO_EVENTS, &events[HOOK_WO_EVENTS_START], - FALSE, INFINITE); - - if (!(event_id >= WAIT_OBJECT_0 && - event_id < WAIT_OBJECT_0 + NUM_HOOK_EVENTS_TOTAL)) { - log("unexpected event id"); - break; - } - - event_id -= WAIT_OBJECT_0; - - switch (event_id) { - case HOOK_WO_EVENT_PING: - log("pinged"); - SetEvent(events[HOOK_EVENT_READY]); - break; - case HOOK_WO_EVENT_START: - start_capture(); - break; - case HOOK_WO_EVENT_STOP: - stop_capture(); - break; - case HOOK_WO_EVENT_SHUTDOWN: - stop_capture(); - shutdown = true; - break; - - default: - log("unexpected event id"); - break; - } - } - -exit: - log("exiting capture thread"); - - if (audioses_module != (uintptr_t)NULL) - CloseHandle((HANDLE)audioses_module); - - destroy_data(); - destroy_events(); - - return 0; -} - -BOOL WINAPI DllMain(HINSTANCE hinst, DWORD reason, LPVOID reserved) -{ - UNUSED_PARAMETER(reserved); - if (reason == DLL_PROCESS_ATTACH) { - log("attach"); - - wchar_t name[MAX_PATH]; - GetModuleFileNameW(hinst, name, MAX_PATH); - LoadLibraryW(name); - - capture_thread = CreateThread( - NULL, 0, (LPTHREAD_START_ROUTINE)main_capture_thread, - NULL, 0, 0); - - if (capture_thread == NULL) - return FALSE; - } else if (reason == DLL_PROCESS_DETACH) { - log("detach"); - - if (capture_thread) { - if (events[HOOK_WO_EVENT_SHUTDOWN] != NULL) - SetEvent(events[HOOK_WO_EVENT_SHUTDOWN]); - - CloseHandle(capture_thread); - } - - if (audioses_module != (uintptr_t)NULL) - CloseHandle((HANDLE)audioses_module); - - destroy_data(); - destroy_events(); - } - - return TRUE; -} - -__declspec(dllexport) LRESULT CALLBACK - dummy_debug_proc(int code, WPARAM wparam, LPARAM lparam) -{ - static bool hooking = true; - - MSG *msg = (MSG *)lparam; - if (hooking && msg->message == (WM_USER + 432)) { - HMODULE user32 = GetModuleHandleW(L"USER32"); - BOOL(WINAPI * unhook_windows_hook_ex)(HHOOK) = NULL; - - unhook_windows_hook_ex = get_obfuscated_func( - user32, "VojeleY`bdgxvM`hhDz", 0x7F55F80C9EE3A213ULL); - - if (unhook_windows_hook_ex) - unhook_windows_hook_ex((HHOOK)msg->lParam); - - hooking = false; - } - - return CallNextHookEx(0, code, wparam, lparam); -} diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..9517e27 --- /dev/null +++ b/src/common.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include + +#include + +#define CALL(punk, method, ...) (punk)->lpVtbl->method((punk), __VA_ARGS__) +#define SAFE_RELEASE(punk) \ + if ((punk) != NULL) { \ + CALL((punk), Release); \ + (punk) = NULL; \ + } + +#define HELPER_DATA_SIZE (1024 * 1024 * 1024) + +#define HELPER_DATA_NAME L"Local\\OBS_ACHelper_Data" + +#define HELPER_WO_EVENT_SHUTDOWN_NAME L"Local\\OBS_ACHelper_WOEventShutdown" +#define HELPER_EVENT_DATA_NAME L"Local\\OBS_ACHelper_EventData" + +#define NUM_HELPER_WO_EVENTS 1 +#define NUM_HELPER_EVENTS 1 +#define NUM_EVENTS 2 + +#define HELPER_WO_EVENTS_START 0 +#define HELPER_WO_EVENTS_END (HELPER_WO_EVENTS_START + NUM_HELPER_WO_EVENTS) + +#define HELPER_EVENTS_START HELPER_WO_EVENTS_END +#define HELPER_EVENTS_END (HELPER_EVENTS_START + NUM_HELPER_EVENTS) + +#define EVENTS_START HELPER_EVENTS_END +#define EVENTS_END (EVENTS_START + NUM_EVENTS) + +#define NUM_HELPER_EVENTS_TOTAL (NUM_HELPER_WO_EVENTS + NUM_HELPER_EVENTS) +#define NUM_EVENTS_TOTAL (NUM_HELPER_EVENTS_TOTAL + NUM_EVENTS) + +enum event { + HELPER_WO_EVENT_SHUTDOWN, + + HELPER_EVENT_DATA, + + EVENT_SHUTDOWN, + EVENT_UPDATE, + + EVENT_PROCESS_TARGET, + EVENT_PROCESS_HELPER, +}; + +static const wchar_t *event_names[NUM_EVENTS_TOTAL] = { + HELPER_WO_EVENT_SHUTDOWN_NAME, HELPER_EVENT_DATA_NAME}; + +static inline void format_name_tag(wchar_t *buf, const wchar_t *name, + const char *tag) +{ + swprintf(buf, MAX_PATH, L"%s_%S", name, tag); +} + +static inline void format_tag(char *buf) +{ + sprintf(buf, "%lu_%lu", GetCurrentProcessId(), GetCurrentThreadId()); +} + +typedef struct audio_capture_helper_data { + long lock; + struct obs_source_audio audio; + + size_t data_size; + uint8_t data[HELPER_DATA_SIZE]; +} audio_capture_helper_data_t; + +static inline void safe_close_handle(HANDLE *handle) +{ + if (*handle != NULL) { + CloseHandle(*handle); + *handle = NULL; + } +} \ No newline at end of file diff --git a/src/get-audio-offsets/CMakeLists.txt b/src/get-audio-offsets/CMakeLists.txt deleted file mode 100644 index a375b3a..0000000 --- a/src/get-audio-offsets/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -project(get-audio-offsets) - -set(get-audio-offsets_SOURCES get-audio-offsets.c uuids.cpp) -add_executable(get-audio-offsets ${get-audio-offsets_SOURCES}) -target_link_libraries(get-audio-offsets mmdevapi.lib) - -if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(_output_suffix "64") -else() - set(_output_suffix "32") -endif() - -set_target_properties(get-audio-offsets PROPERTIES - OUTPUT_NAME "get-audio-offsets${_output_suffix}") - -copy_helper_lib(get-audio-offsets) \ No newline at end of file diff --git a/src/get-audio-offsets/get-audio-offsets.c b/src/get-audio-offsets/get-audio-offsets.c deleted file mode 100644 index 5fe1b8b..0000000 --- a/src/get-audio-offsets/get-audio-offsets.c +++ /dev/null @@ -1,228 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include - -#include -#include - -#include "uuids.h" - -#define REFTIMES_PER_SEC 10000000 -#define REFTIMES_PER_MILLISEC 10000 - -#define SAFE_RELEASE(punk) \ - if ((punk) != NULL) { \ - (punk)->lpVtbl->Release(punk); \ - (punk) = NULL; \ - } - -typedef struct wasapi_info { - IAudioClient *client; - IAudioRenderClient *render_client; - - WAVEFORMATEX *format; -} wasapi_info_t; - -static inline HRESULT wasapi_info_init(wasapi_info_t *info) -{ - HRESULT hr = E_FAIL; - - uuids_t uuids = get_uuids(); - - REFERENCE_TIME requested_duration = REFTIMES_PER_SEC; - REFERENCE_TIME actual_duration; - - IMMDeviceEnumerator *enumerator = NULL; - hr = CoCreateInstance(&uuids.CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, - &uuids.IID_IMMDeviceEnumerator, - (void **)&enumerator); - - if (FAILED(hr)) { - printf("failed to create device enumerator\n"); - goto exit; - } - - IMMDevice *device = NULL; - hr = enumerator->lpVtbl->GetDefaultAudioEndpoint(enumerator, eRender, - eConsole, &device); - - if (FAILED(hr)) { - printf("failed get default endpoint\n"); - goto exit; - } - - hr = device->lpVtbl->Activate(device, &uuids.IID_IAudioClient, - CLSCTX_ALL, NULL, (void **)&info->client); - - if (FAILED(hr)) { - printf("failed to get client\n"); - goto exit; - } - - hr = info->client->lpVtbl->GetMixFormat(info->client, &info->format); - - if (FAILED(hr)) { - printf("failed to get mix format\n"); - goto exit; - } - - hr = info->client->lpVtbl->Initialize(info->client, - AUDCLNT_SHAREMODE_SHARED, 0, - requested_duration, 0, - info->format, NULL); - - if (FAILED(hr)) { - printf("failed to initialize client\n"); - goto exit; - } - - hr = info->client->lpVtbl->GetService(info->client, - &uuids.IID_IAudioRenderClient, - (void **)&info->render_client); - - if (FAILED(hr)) { - printf("failed to get render client\n"); - goto exit; - } - -exit: - - SAFE_RELEASE(enumerator); - SAFE_RELEASE(device); - - return hr; -} - -static inline void wasapi_info_destroy(wasapi_info_t *info) -{ - CoTaskMemFree(info->format); - - SAFE_RELEASE(info->render_client); - SAFE_RELEASE(info->client); -} - -static MODULEINFO get_module_info(HMODULE module) -{ - MODULEINFO info; - GetModuleInformation(GetCurrentProcess(), module, &info, sizeof(info)); - - return info; -} - -static bool is_ptr_in_module(HMODULE module, uintptr_t func) -{ - MODULEINFO info = get_module_info(module); - - return (func > (uintptr_t)info.lpBaseOfDll) && - (func < (uintptr_t)info.lpBaseOfDll + info.SizeOfImage); -} - -static inline uint32_t vtable_offset(HMODULE module, void *cls, - unsigned int offset) -{ - uintptr_t *vtable = *(uintptr_t **)cls; - return (uint32_t)(vtable[offset] - (uintptr_t)module); -} - -static uint32_t scan_render_client_client_offset(wasapi_info_t *info) -{ - char *client_candidate = (char *)info->render_client; - for (int i = 1; i < 32 * 8; ++i) { - IAudioClient **client_ptr = - (IAudioClient **)&client_candidate[i]; - - if (*client_ptr == info->client) - return i; - } - - return 0; -} - -static bool wave_formats_equal(WAVEFORMATEX *w1, WAVEFORMATEX *w2) -{ - __try { - int neq = memcmp(w1, w2, sizeof(WAVEFORMATEX)); - return !neq; - } __except (EXCEPTION_EXECUTE_HANDLER) { - } - - return false; -} - -static uint32_t scan_render_client_format_offset(wasapi_info_t *info) -{ - char *format_candidate = (char *)info->render_client; - for (int i = 1; i < 32 * 8; ++i) { - WAVEFORMATEX **format_ptr = - (WAVEFORMATEX **)&format_candidate[i]; - - if (wave_formats_equal(*format_ptr, info->format)) - return i; - } - - printf("Format pointer not found\n"); - return 0; -} - -int main() -{ - int ret = 1; - - if (FAILED(CoInitializeEx(NULL, COINIT_MULTITHREADED))) { - printf("failed to initialize COM library"); - return ret; - }; - - wasapi_info_t info; - HRESULT hr = wasapi_info_init(&info); - - if (FAILED(hr)) { - printf("wasapi init failed: %ld\n", hr); - goto exit; - } - - uintptr_t client_vtbl = (uintptr_t)info.client->lpVtbl; - uintptr_t render_client_vtbl = (uintptr_t)info.render_client->lpVtbl; - - uint32_t render_client_client_offset = - scan_render_client_client_offset(&info); - - uint32_t render_client_format_offset = - scan_render_client_format_offset(&info); - - HMODULE module = GetModuleHandleW(L"audioses.dll"); - if (module == NULL) { - printf("failed to find loaded audioses.dll\n"); - goto exit; - } - - uintptr_t module_addr = (uintptr_t)get_module_info(module).lpBaseOfDll; - - if (!is_ptr_in_module(module, client_vtbl)) { - printf("client vtbl not in found audioses.dll!\n"); - goto exit; - } - - if (!is_ptr_in_module(module, render_client_vtbl)) { - printf("render client vtbl not in found audioses.dll!\n"); - goto exit; - } - - printf("[wasapi]\n"); - printf("client_vtbl=0x%x\n", (uint32_t)(client_vtbl - module_addr)); - printf("render_client_vtbl=0x%x\n", - (uint32_t)(render_client_vtbl - module_addr)); - printf("m_render_client_client=0x%x\n", render_client_client_offset); - printf("m_render_client_format=0x%x\n", render_client_format_offset); - - ret = 0; - -exit: - wasapi_info_destroy(&info); - return ret; -} \ No newline at end of file diff --git a/src/get-audio-offsets/uuids.cpp b/src/get-audio-offsets/uuids.cpp deleted file mode 100644 index 740286a..0000000 --- a/src/get-audio-offsets/uuids.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include -#include - -#include "uuids.h" - -extern "C" { - -uuids_t get_uuids() -{ - return { - __uuidof(MMDeviceEnumerator), - __uuidof(IMMDeviceEnumerator), - __uuidof(IAudioClient), - __uuidof(IAudioRenderClient), - }; -} -} diff --git a/src/hook-info.h b/src/hook-info.h deleted file mode 100644 index a7f7174..0000000 --- a/src/hook-info.h +++ /dev/null @@ -1,116 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - - -/* clang-format off */ - -#define HOOK_DATA_SIZE (1024 * 1024 * 1024) - -#define HOOK_DATA_NAME L"Local\\OBS_ACHook_Data" -#define HOOK_METADATA_NAME L"Local\\OBS_ACHook_Metadata" - -#define HOOK_WO_EVENT_PING_NAME L"Local\\OBS_ACHook_WOEventPing" -#define HOOK_WO_EVENT_START_NAME L"Local\\OBS_ACHook_WOEventStart" -#define HOOK_WO_EVENT_STOP_NAME L"Local\\OBS_ACHook_WOEventStop" -#define HOOK_WO_EVENT_SHUTDOWN_NAME L"Local\\OBS_ACHook_WOEventShutdown" - -#define HOOK_EVENT_READY_NAME L"Local\\OBS_ACHook_EventReady" -#define HOOK_EVENT_ACTIVE_NAME L"Local\\OBS_ACHook_EventActive" -#define HOOK_EVENT_DATA_NAME L"Local\\OBS_ACHook_EventData" - -#define NUM_HOOK_WO_EVENTS 4 -#define NUM_HOOK_EVENTS 3 -#define NUM_EVENTS 3 - -#define HOOK_WO_EVENTS_START 0 -#define HOOK_WO_EVENTS_END (HOOK_WO_EVENTS_START + NUM_HOOK_WO_EVENTS) - -#define HOOK_EVENTS_START HOOK_WO_EVENTS_END -#define HOOK_EVENTS_END (HOOK_EVENTS_START + NUM_HOOK_EVENTS) - -#define EVENTS_START HOOK_EVENTS_END -#define EVENTS_END (EVENTS_START + NUM_EVENTS) - -#define NUM_HOOK_EVENTS_TOTAL (NUM_HOOK_WO_EVENTS + NUM_HOOK_EVENTS) -#define NUM_EVENTS_TOTAL (NUM_HOOK_EVENTS_TOTAL + NUM_EVENTS) - -/* clang-format on */ - -enum event { - HOOK_WO_EVENT_PING, - HOOK_WO_EVENT_START, - HOOK_WO_EVENT_STOP, - HOOK_WO_EVENT_SHUTDOWN, - - HOOK_EVENT_READY, - HOOK_EVENT_ACTIVE, - HOOK_EVENT_DATA, - - EVENT_SHUTDOWN, - EVENT_REHOOK, - EVENT_UPDATE, -}; - -typedef struct event_info { - wchar_t *name; - bool reset; -} event_info_t; - -static event_info_t event_info[NUM_EVENTS_TOTAL] = { - {HOOK_WO_EVENT_PING_NAME, FALSE}, - {HOOK_WO_EVENT_START_NAME, FALSE}, - {HOOK_WO_EVENT_STOP_NAME, FALSE}, - {HOOK_WO_EVENT_SHUTDOWN_NAME, FALSE}, - - {HOOK_EVENT_READY_NAME, FALSE}, - {HOOK_EVENT_ACTIVE_NAME, FALSE}, - {HOOK_EVENT_DATA_NAME, FALSE}, - - {NULL, FALSE}, - {NULL, FALSE}, - {NULL, FALSE}, -}; - -typedef struct audio_hook_offsets { - struct { - uint32_t client_vtbl; - uint32_t render_client_vtbl; - - uint32_t m_render_client_client; - uint32_t m_render_client_format; - } wasapi; -} audio_hook_offsets_t; - -typedef struct audio_hook_metadata { - long initialized; - - audio_hook_offsets_t offsets32; - audio_hook_offsets_t offsets64; -} audio_hook_metadata_t; - -typedef struct audio_hook_data { - long lock; - - WAVEFORMATEXTENSIBLE format; - - uint64_t timestamp; - uint32_t frames; - - uint8_t data[HOOK_DATA_SIZE]; -} audio_hook_data_t; - -static inline void format_name_pid(wchar_t *buf, const wchar_t *name, DWORD pid) -{ - swprintf(buf, MAX_PATH, L"%s%lu", name, pid); -} - -static inline void format_name(wchar_t *buf, const wchar_t *name) -{ - format_name_pid(buf, name, GetCurrentProcessId()); -} diff --git a/src/inject-helper/CMakeLists.txt b/src/inject-helper/CMakeLists.txt deleted file mode 100644 index 757060c..0000000 --- a/src/inject-helper/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -project(inject-helper) - -set(inject-helper_HEADERS - ../inject-library.h - ../obfuscate.h) - -set(inject-helper_SOURCES - ../inject-library.c - ../obfuscate.c - inject-helper.c) - -add_executable(inject-helper - ${inject-helper_SOURCES}) - -if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(_output_suffix "64") -else() - set(_output_suffix "32") -endif() - -set_target_properties(inject-helper - PROPERTIES OUTPUT_NAME "inject-helper${_output_suffix}") - -copy_helper_lib(inject-helper) \ No newline at end of file diff --git a/src/inject-helper/inject-helper.c b/src/inject-helper/inject-helper.c deleted file mode 100644 index b354ea6..0000000 --- a/src/inject-helper/inject-helper.c +++ /dev/null @@ -1,122 +0,0 @@ -#include -#include -#include - -#include -#include -#include - -#include "../obfuscate.h" -#include "../inject-library.h" - -#if defined(_MSC_VER) && !defined(inline) -#define inline __inline -#endif - -static void load_debug_privilege(void) -{ - const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; - TOKEN_PRIVILEGES tp; - HANDLE token; - LUID val; - - if (!OpenProcessToken(GetCurrentProcess(), flags, &token)) { - return; - } - - if (!!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &val)) { - tp.PrivilegeCount = 1; - tp.Privileges[0].Luid = val; - tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - - AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, - NULL); - } - - CloseHandle(token); -} - -static inline HANDLE open_process(DWORD desired_access, bool inherit_handle, - DWORD process_id) -{ - HANDLE(WINAPI * open_process_proc)(DWORD, BOOL, DWORD); - open_process_proc = get_obfuscated_func(GetModuleHandleW(L"KERNEL32"), - "HxjcQrmkb|~", - 0xc82efdf78201df87); - - return open_process_proc(desired_access, inherit_handle, process_id); -} - -static inline int inject_library(HANDLE process, const wchar_t *dll) -{ - return inject_library_obf(process, dll, "E}mo|d[cefubWk~bgk", - 0x7c3371986918e8f6, "Rqbr`T{cnor{Bnlgwz", - 0x81bf81adc9456b35, "]`~wrl`KeghiCt", - 0xadc6a7b9acd73c9b, "Zh}{}agHzfd@{", - 0x57135138eb08ff1c, "DnafGhj}l~sX", - 0x350bfacdf81b2018); -} - -static inline int inject_library_safe(DWORD thread_id, const wchar_t *dll) -{ - return inject_library_safe_obf(thread_id, dll, "[bs^fbkmwuKfmfOvI", - 0xEAD293602FCF9778ULL); -} - -static inline int inject_library_full(DWORD process_id, const wchar_t *dll) -{ - HANDLE process = open_process(PROCESS_ALL_ACCESS, false, process_id); - int ret; - - if (process) { - ret = inject_library(process, dll); - CloseHandle(process); - } else { - ret = INJECT_ERROR_OPEN_PROCESS_FAIL; - } - - return ret; -} - -static int inject_helper(wchar_t *argv[], const wchar_t *dll) -{ - DWORD id; - DWORD use_safe_inject; - - use_safe_inject = wcstol(argv[2], NULL, 10); - - id = wcstol(argv[3], NULL, 10); - if (id == 0) { - return INJECT_ERROR_INVALID_PARAMS; - } - - return use_safe_inject ? inject_library_safe(id, dll) - : inject_library_full(id, dll); -} - -#define UNUSED_PARAMETER(x) ((void)(x)) - -int main(int argc, char *argv_ansi[]) -{ - wchar_t dll_path[MAX_PATH]; - LPWSTR pCommandLineW; - LPWSTR *argv; - - int ret = INJECT_ERROR_INVALID_PARAMS; - - SetErrorMode(SEM_FAILCRITICALERRORS); - load_debug_privilege(); - - pCommandLineW = GetCommandLineW(); - argv = CommandLineToArgvW(pCommandLineW, &argc); - if (argv && argc == 4) { - DWORD size = GetModuleFileNameW(NULL, dll_path, MAX_PATH); - if (size) { - ret = inject_helper(argv, argv[1]); - } - } - LocalFree(argv); - - UNUSED_PARAMETER(argv_ansi); - return ret; -} diff --git a/src/inject-library.c b/src/inject-library.c deleted file mode 100644 index 4710587..0000000 --- a/src/inject-library.c +++ /dev/null @@ -1,150 +0,0 @@ -#include -#include - -#include "obfuscate.h" -#include "inject-library.h" - -typedef HANDLE(WINAPI *create_remote_thread_t)(HANDLE, LPSECURITY_ATTRIBUTES, - SIZE_T, LPTHREAD_START_ROUTINE, - LPVOID, DWORD, LPDWORD); -typedef BOOL(WINAPI *write_process_memory_t)(HANDLE, LPVOID, LPCVOID, SIZE_T, - SIZE_T *); -typedef LPVOID(WINAPI *virtual_alloc_ex_t)(HANDLE, LPVOID, SIZE_T, DWORD, - DWORD); -typedef BOOL(WINAPI *virtual_free_ex_t)(HANDLE, LPVOID, SIZE_T, DWORD); - -int inject_library_obf(HANDLE process, const wchar_t *dll, - const char *create_remote_thread_obf, uint64_t obf1, - const char *write_process_memory_obf, uint64_t obf2, - const char *virtual_alloc_ex_obf, uint64_t obf3, - const char *virtual_free_ex_obf, uint64_t obf4, - const char *load_library_w_obf, uint64_t obf5) -{ - int ret = INJECT_ERROR_UNLIKELY_FAIL; - DWORD last_error = 0; - bool success = false; - size_t written_size; - DWORD thread_id; - HANDLE thread = NULL; - size_t size; - void *mem; - - /* -------------------------------- */ - - HMODULE kernel32 = GetModuleHandleW(L"KERNEL32"); - create_remote_thread_t create_remote_thread; - write_process_memory_t write_process_memory; - virtual_alloc_ex_t virtual_alloc_ex; - virtual_free_ex_t virtual_free_ex; - FARPROC load_library_w; - - create_remote_thread = (create_remote_thread_t)get_obfuscated_func( - kernel32, create_remote_thread_obf, obf1); - write_process_memory = (write_process_memory_t)get_obfuscated_func( - kernel32, write_process_memory_obf, obf2); - virtual_alloc_ex = (virtual_alloc_ex_t)get_obfuscated_func( - kernel32, virtual_alloc_ex_obf, obf3); - virtual_free_ex = (virtual_free_ex_t)get_obfuscated_func( - kernel32, virtual_free_ex_obf, obf4); - load_library_w = (FARPROC)get_obfuscated_func(kernel32, - load_library_w_obf, obf5); - - /* -------------------------------- */ - - size = (wcslen(dll) + 1) * sizeof(wchar_t); - mem = virtual_alloc_ex(process, NULL, size, MEM_RESERVE | MEM_COMMIT, - PAGE_READWRITE); - if (!mem) { - goto fail; - } - - success = write_process_memory(process, mem, dll, size, &written_size); - if (!success) { - goto fail; - } - - thread = create_remote_thread(process, NULL, 0, - (LPTHREAD_START_ROUTINE)load_library_w, - mem, 0, &thread_id); - if (!thread) { - goto fail; - } - - if (WaitForSingleObject(thread, 4000) == WAIT_OBJECT_0) { - DWORD code; - GetExitCodeThread(thread, &code); - - ret = (code != 0) ? 0 : INJECT_ERROR_INJECT_FAILED; - SetLastError(0); - } - -fail: - if (ret == INJECT_ERROR_UNLIKELY_FAIL) { - last_error = GetLastError(); - } - if (thread) { - CloseHandle(thread); - } - if (mem) { - virtual_free_ex(process, mem, 0, MEM_RELEASE); - } - if (last_error != 0) { - SetLastError(last_error); - } - - return ret; -} - -/* ------------------------------------------------------------------------- */ - -typedef HHOOK(WINAPI *set_windows_hook_ex_t)(int, HOOKPROC, HINSTANCE, DWORD); - -#define RETRY_INTERVAL_MS 500 -#define TOTAL_RETRY_TIME_MS 4000 -#define RETRY_COUNT (TOTAL_RETRY_TIME_MS / RETRY_INTERVAL_MS) - -int inject_library_safe_obf(DWORD thread_id, const wchar_t *dll, - const char *set_windows_hook_ex_obf, uint64_t obf1) -{ - HMODULE user32 = GetModuleHandleW(L"USER32"); - set_windows_hook_ex_t set_windows_hook_ex; - HMODULE lib = LoadLibraryW(dll); - HOOKPROC proc; - HHOOK hook; - size_t i; - - if (!lib || !user32) { - return INJECT_ERROR_UNLIKELY_FAIL; - } - -#ifdef _WIN64 - proc = (HOOKPROC)GetProcAddress(lib, "dummy_debug_proc"); -#else - proc = (HOOKPROC)GetProcAddress(lib, "_dummy_debug_proc@12"); -#endif - - if (!proc) { - return INJECT_ERROR_UNLIKELY_FAIL; - } - - set_windows_hook_ex = (set_windows_hook_ex_t)get_obfuscated_func( - user32, set_windows_hook_ex_obf, obf1); - - hook = set_windows_hook_ex(WH_GETMESSAGE, proc, lib, thread_id); - if (!hook) { - return GetLastError(); - } - - /* SetWindowsHookEx does not inject the library in to the target - * process unless the event associated with it has occurred, so - * repeatedly send the hook message to start the hook at small - * intervals to signal to SetWindowsHookEx to process the message and - * therefore inject the library in to the target process. Repeating - * this is mostly just a precaution. */ - - for (i = 0; i < RETRY_COUNT; i++) { - Sleep(RETRY_INTERVAL_MS); - PostThreadMessage(thread_id, WM_USER + 432, 0, (LPARAM)hook); - } - return 0; -} diff --git a/src/inject-library.h b/src/inject-library.h deleted file mode 100644 index f65a129..0000000 --- a/src/inject-library.h +++ /dev/null @@ -1,20 +0,0 @@ -#include -#include - -#define INJECT_ERROR_INJECT_FAILED -1 -#define INJECT_ERROR_INVALID_PARAMS -2 -#define INJECT_ERROR_OPEN_PROCESS_FAIL -3 -#define INJECT_ERROR_UNLIKELY_FAIL -4 - -extern int inject_library_obf(HANDLE process, const wchar_t *dll, - const char *create_remote_thread_obf, - uint64_t obf1, - const char *write_process_memory_obf, - uint64_t obf2, const char *virtual_alloc_ex_obf, - uint64_t obf3, const char *virtual_free_ex_obf, - uint64_t obf4, const char *load_library_w_obf, - uint64_t obf5); - -extern int inject_library_safe_obf(DWORD thread_id, const wchar_t *dll, - const char *set_windows_hook_ex_obf, - uint64_t obf1); diff --git a/src/plugin.c b/src/plugin.c index 065bb6c..6ed4a3d 100644 --- a/src/plugin.c +++ b/src/plugin.c @@ -4,7 +4,6 @@ #include #include -#include "hook-info.h" #include "util/base.h" #include "util/pipe.h" @@ -13,174 +12,10 @@ OBS_MODULE_USE_DEFAULT_LOCALE("win-capture-audio", "en-GB") extern struct obs_source_info audio_capture_info; -HANDLE init_hook_metadata_thread; - -HANDLE hook_metadata_map; -volatile audio_hook_metadata_t *hook_metadata; - -static inline bool is_64bit_windows(void) -{ -#ifdef _WIN64 - return true; -#else - BOOL x86 = false; - bool success = !!IsWow64Process(GetCurrentProcess(), &x86); - return success && !!x86; -#endif -} - -static char *read_cmd_output(char *cmd) -{ - os_process_pipe_t *pipe = os_process_pipe_create(cmd, "r"); - - if (!pipe) - return NULL; - - struct dstr output = {0}; - - while (true) { - char data[2048]; - size_t len = os_process_pipe_read(pipe, (uint8_t *)data, - sizeof(data)); - if (len == 0) - break; - - dstr_ncat(&output, data, len); - } - - if (dstr_is_empty(&output)) - return NULL; - - return output.array; -} - -static bool load_offsets_from_string(volatile audio_hook_offsets_t *offsets, - const char *str) -{ - config_t *config; - - if (config_open_string(&config, str) != CONFIG_SUCCESS) - return false; - - offsets->wasapi.client_vtbl = - (uint32_t)config_get_uint(config, "wasapi", "client_vtbl"); - offsets->wasapi.render_client_vtbl = (uint32_t)config_get_uint( - config, "wasapi", "render_client_vtbl"); - offsets->wasapi.m_render_client_client = (uint32_t)config_get_uint( - config, "wasapi", "m_render_client_client"); - offsets->wasapi.m_render_client_format = (uint32_t)config_get_uint( - config, "wasapi", "m_render_client_format"); - - config_close(config); - - return !(offsets->wasapi.client_vtbl == 0 || - offsets->wasapi.render_client_vtbl == 0 || - offsets->wasapi.m_render_client_client == 0 || - offsets->wasapi.m_render_client_format == 0); -} - -static bool load_offsets_from_exe(volatile audio_hook_offsets_t *offsets, - const char *exe) -{ - char *offsets_cmd = obs_module_file(exe); - char *offsets_str = read_cmd_output(offsets_cmd); - - bool success = false; - - if (offsets_str == NULL) { - blog(LOG_ERROR, "[audio-capture] failed run %s", exe); - goto exit; - } - - if (!load_offsets_from_string(offsets, offsets_str)) { - blog(LOG_ERROR, - "[audio-capture] failed to load audio offsets from %s", - exe); - goto exit; - } - - success = true; - -exit: - bfree(offsets_cmd); - bfree(offsets_str); - - return success; -} - -static DWORD WINAPI init_hook_metadata(LPVOID param) -{ - UNUSED_PARAMETER(param); - - - if (!load_offsets_from_exe(&hook_metadata->offsets32, - "get-audio-offsets32.exe")) { - blog(LOG_ERROR, - "[audio-capture] failed to load 32-bit audio offsets"); - return 1; - } - - if (is_64bit_windows() && - !load_offsets_from_exe(&hook_metadata->offsets64, - "get-audio-offsets64.exe")) { - blog(LOG_ERROR, - "[audio-capture] failed to load 64-bit audio offsets"); - return 1; - } - - - hook_metadata->initialized = true; - return 0; -} - bool obs_module_load(void) { - hook_metadata_map = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, - PAGE_READWRITE, 0, - sizeof(audio_hook_metadata_t), - HOOK_METADATA_NAME); - - if (hook_metadata_map == NULL) { - blog(LOG_ERROR, - "[audio-capture] failed to create hook metadata map"); - return false; - } - - hook_metadata = MapViewOfFile(hook_metadata_map, FILE_MAP_ALL_ACCESS, 0, - 0, sizeof(audio_hook_metadata_t)); - - if (hook_metadata == NULL) { - blog(LOG_ERROR, - "[audio-capture] failed to create hook metadata view"); - - CloseHandle(hook_metadata_map); - return false; - } - - init_hook_metadata_thread = - CreateThread(NULL, 0, init_hook_metadata, NULL, 0, NULL); - - if (init_hook_metadata_thread == NULL) { - blog(LOG_ERROR, - "[audio-capture] failed to create hook metadata " - "init thread"); - - UnmapViewOfFile((void *)hook_metadata); - CloseHandle(hook_metadata_map); - return false; - } - obs_register_source(&audio_capture_info); return true; } -void obs_module_unload() -{ - if (init_hook_metadata_thread == NULL) - return; - - WaitForSingleObject(init_hook_metadata_thread, INFINITE); - - UnmapViewOfFile((void *)hook_metadata); - CloseHandle(hook_metadata_map); -} \ No newline at end of file +void obs_module_unload() {} \ No newline at end of file diff --git a/src/window-helpers.c b/src/window-helpers.c index 9eb8234..4a3219b 100644 --- a/src/window-helpers.c +++ b/src/window-helpers.c @@ -1,12 +1,236 @@ -#include #define PSAPI_VERSION 1 #include #include +#include +#include + #include "window-helpers.h" #include "obfuscate.h" +extern void window_info_destroy(window_info_t *info) +{ + bfree(info->executable); + bfree(info->class); + bfree(info->title); +} + +extern bool window_info_cmp(window_info_t *info_a, window_info_t *info_b) +{ + + if (info_a->title == NULL && info_b->title == NULL) + return false; + else if (info_a->title == NULL || info_b->title == NULL) + return true; + + return strcmp(info_a->executable, info_b->executable) || + strcmp(info_a->class, info_b->class) || + strcmp(info_a->title, info_b->title); +} + +static bool check_window_valid(HWND window, enum window_search_mode mode) +{ + DWORD styles, ex_styles; + RECT rect; + + if (!IsWindowVisible(window) || + (mode == EXCLUDE_MINIMIZED && IsIconic(window))) + return false; + + GetClientRect(window, &rect); + styles = (DWORD)GetWindowLongPtr(window, GWL_STYLE); + ex_styles = (DWORD)GetWindowLongPtr(window, GWL_EXSTYLE); + + if (ex_styles & WS_EX_TOOLWINDOW) + return false; + if (styles & WS_CHILD) + return false; + if (mode == EXCLUDE_MINIMIZED && (rect.bottom == 0 || rect.right == 0)) + return false; + + return true; +} + +static HWND next_window(HWND window, enum window_search_mode mode, HWND *parent, + bool use_findwindowex) +{ + if (*parent) { + window = *parent; + *parent = NULL; + } + + while (true) { + if (use_findwindowex) + window = FindWindowEx(GetDesktopWindow(), window, NULL, + NULL); + else + window = GetNextWindow(window, GW_HWNDNEXT); + + if (!window || check_window_valid(window, mode)) + break; + } + + if (is_uwp_window(window)) { + HWND child = get_uwp_actual_window(window); + if (child) { + *parent = window; + return child; + } + } + + return window; +} + +static HWND first_window(enum window_search_mode mode, HWND *parent, + bool *use_findwindowex) +{ + HWND window = FindWindowEx(GetDesktopWindow(), NULL, NULL, NULL); + + if (!window) { + *use_findwindowex = false; + window = GetWindow(GetDesktopWindow(), GW_CHILD); + } else { + *use_findwindowex = true; + } + + *parent = NULL; + + if (!check_window_valid(window, mode)) { + window = next_window(window, mode, parent, *use_findwindowex); + + if (!window && *use_findwindowex) { + *use_findwindowex = false; + + window = GetWindow(GetDesktopWindow(), GW_CHILD); + if (!check_window_valid(window, mode)) + window = next_window(window, mode, parent, + *use_findwindowex); + } + } + + if (is_uwp_window(window)) { + HWND child = get_uwp_actual_window(window); + if (child) { + *parent = window; + return child; + } + } + + return window; +} + +static const char *generic_class_substrings[] = { + "Chrome", + NULL, +}; + +static const char *generic_classes[] = { + "Windows.UI.Core.CoreWindow", + NULL, +}; + +static bool is_generic_class(const char *current_class) +{ + const char **class = generic_class_substrings; + while (*class) { + if (astrstri(current_class, *class) != NULL) + return true; + + class ++; + } + + class = generic_classes; + while (*class) { + if (astrcmpi(current_class, *class) == 0) + return true; + + class ++; + } + + return false; +} + +static int window_rating(HWND window, enum window_priority priority, + const window_info_t *info, bool generic_class) +{ + struct dstr cur_class = {0}; + struct dstr cur_title = {0}; + struct dstr cur_exe = {0}; + int val = INT_MAX; + + if (!get_window_exe(&cur_exe, window)) + return INT_MAX; + + get_window_title(&cur_title, window); + get_window_class(&cur_class, window); + + bool exe_matches = dstr_cmpi(&cur_exe, info->executable) == 0; + bool class_matches = dstr_cmpi(&cur_class, info->class) == 0; + int title_val = abs(dstr_cmpi(&cur_title, info->title)); + + /* always match by name if class is generic */ + if (priority == WINDOW_PRIORITY_CLASS && generic_class) { + val = title_val == 0 ? 0 : INT_MAX; + + } else if (priority == WINDOW_PRIORITY_CLASS) { + val = class_matches ? title_val : INT_MAX; + if (val != INT_MAX && !exe_matches) + val += 0x1000; + + } else if (priority == WINDOW_PRIORITY_TITLE) { + val = title_val == 0 ? 0 : INT_MAX; + + } else if (priority == WINDOW_PRIORITY_EXE) { + val = exe_matches ? title_val : INT_MAX; + } + + dstr_free(&cur_class); + dstr_free(&cur_title); + dstr_free(&cur_exe); + + return val; +} + +extern HWND window_info_get_window(window_info_t *info, + enum window_priority priority) +{ + if (strcmp(info->class, "dwm") == 0) { + wchar_t class_w[512]; + os_utf8_to_wcs(info->class, 0, class_w, 512); + return FindWindowW(class_w, NULL); + } + + HWND parent; + bool use_findwindowex = false; + + HWND window = + first_window(INCLUDE_MINIMIZED, &parent, &use_findwindowex); + HWND best_window = NULL; + int best_rating = 0x7FFFFFFF; + + if (!info->class) + return NULL; + + bool generic_class = is_generic_class(info->class); + + while (window) { + int rating = + window_rating(window, priority, info, generic_class); + if (rating < best_rating) { + best_rating = rating; + best_window = window; + if (rating == 0) + break; + } + + window = next_window(window, INCLUDE_MINIMIZED, &parent, + use_findwindowex); + } + + return best_window; +} + static inline void encode_dstr(struct dstr *str) { dstr_replace(str, "#", "#22"); @@ -22,14 +246,13 @@ static inline char *decode_str(const char *src) return str.array; } -extern void build_window_strings(const char *str, char **class, char **title, - char **exe) +extern void build_window_strings(const char *str, window_info_t *info) { char **strlist; - *class = NULL; - *title = NULL; - *exe = NULL; + info->class = NULL; + info->title = NULL; + info->executable = NULL; if (!str) { return; @@ -38,9 +261,9 @@ extern void build_window_strings(const char *str, char **class, char **title, strlist = strlist_split(str, ':', true); if (strlist && strlist[0] && strlist[1] && strlist[2]) { - *title = decode_str(strlist[0]); - *class = decode_str(strlist[1]); - *exe = decode_str(strlist[2]); + info->title = decode_str(strlist[0]); + info->class = decode_str(strlist[1]); + info->executable = decode_str(strlist[2]); } strlist_free(strlist); @@ -177,14 +400,10 @@ static const char *blacklisted_exes[] = { "steam.exe", "battle.net.exe", "galaxyclient.exe", - "skype.exe", "uplay.exe", "origin.exe", "devenv.exe", "taskmgr.exe", - "chrome.exe", - "discord.exe", - "firefox.exe", "systemsettings.exe", "applicationframehost.exe", "cmd.exe", @@ -262,29 +481,6 @@ static void add_window(obs_property_t *p, HWND hwnd, add_window_cb callback) dstr_free(&exe); } -static bool check_window_valid(HWND window, enum window_search_mode mode) -{ - DWORD styles, ex_styles; - RECT rect; - - if (!IsWindowVisible(window) || - (mode == EXCLUDE_MINIMIZED && IsIconic(window))) - return false; - - GetClientRect(window, &rect); - styles = (DWORD)GetWindowLongPtr(window, GWL_STYLE); - ex_styles = (DWORD)GetWindowLongPtr(window, GWL_EXSTYLE); - - if (ex_styles & WS_EX_TOOLWINDOW) - return false; - if (styles & WS_CHILD) - return false; - if (mode == EXCLUDE_MINIMIZED && (rect.bottom == 0 || rect.right == 0)) - return false; - - return true; -} - bool is_uwp_window(HWND hwnd) { wchar_t name[256]; @@ -317,74 +513,6 @@ HWND get_uwp_actual_window(HWND parent) return NULL; } -static HWND next_window(HWND window, enum window_search_mode mode, HWND *parent, - bool use_findwindowex) -{ - if (*parent) { - window = *parent; - *parent = NULL; - } - - while (true) { - if (use_findwindowex) - window = FindWindowEx(GetDesktopWindow(), window, NULL, - NULL); - else - window = GetNextWindow(window, GW_HWNDNEXT); - - if (!window || check_window_valid(window, mode)) - break; - } - - if (is_uwp_window(window)) { - HWND child = get_uwp_actual_window(window); - if (child) { - *parent = window; - return child; - } - } - - return window; -} - -static HWND first_window(enum window_search_mode mode, HWND *parent, - bool *use_findwindowex) -{ - HWND window = FindWindowEx(GetDesktopWindow(), NULL, NULL, NULL); - - if (!window) { - *use_findwindowex = false; - window = GetWindow(GetDesktopWindow(), GW_CHILD); - } else { - *use_findwindowex = true; - } - - *parent = NULL; - - if (!check_window_valid(window, mode)) { - window = next_window(window, mode, parent, *use_findwindowex); - - if (!window && *use_findwindowex) { - *use_findwindowex = false; - - window = GetWindow(GetDesktopWindow(), GW_CHILD); - if (!check_window_valid(window, mode)) - window = next_window(window, mode, parent, - *use_findwindowex); - } - } - - if (is_uwp_window(window)) { - HWND child = get_uwp_actual_window(window); - if (child) { - *parent = window; - return child; - } - } - - return window; -} - void fill_window_list(obs_property_t *p, enum window_search_mode mode, add_window_cb callback) { @@ -397,162 +525,4 @@ void fill_window_list(obs_property_t *p, enum window_search_mode mode, add_window(p, window, callback); window = next_window(window, mode, &parent, use_findwindowex); } -} - -static int window_rating(HWND window, enum window_priority priority, - const char *class, const char *title, const char *exe, - bool generic_class) -{ - struct dstr cur_class = {0}; - struct dstr cur_title = {0}; - struct dstr cur_exe = {0}; - int val = INT_MAX; - - if (!get_window_exe(&cur_exe, window)) - return INT_MAX; - get_window_title(&cur_title, window); - get_window_class(&cur_class, window); - - bool class_matches = dstr_cmpi(&cur_class, class) == 0; - bool exe_matches = dstr_cmpi(&cur_exe, exe) == 0; - int title_val = abs(dstr_cmpi(&cur_title, title)); - - /* always match by name if class is generic */ - if (priority == WINDOW_PRIORITY_CLASS && generic_class) { - val = title_val == 0 ? 0 : INT_MAX; - - } else if (priority == WINDOW_PRIORITY_CLASS) { - val = class_matches ? title_val : INT_MAX; - if (val != INT_MAX && !exe_matches) - val += 0x1000; - - } else if (priority == WINDOW_PRIORITY_TITLE) { - val = title_val == 0 ? 0 : INT_MAX; - - } else if (priority == WINDOW_PRIORITY_EXE) { - val = exe_matches ? title_val : INT_MAX; - } - - dstr_free(&cur_class); - dstr_free(&cur_title); - dstr_free(&cur_exe); - - return val; -} - -static const char *generic_class_substrings[] = { - "Chrome", - NULL, -}; - -static const char *generic_classes[] = { - "Windows.UI.Core.CoreWindow", - NULL, -}; - -static bool is_generic_class(const char *current_class) -{ - const char **class = generic_class_substrings; - while (*class) { - if (astrstri(current_class, *class) != NULL) { - return true; - } - class ++; - } - - class = generic_classes; - while (*class) { - if (astrcmpi(current_class, *class) == 0) { - return true; - } - class ++; - } - - return false; -} - -HWND find_window(enum window_search_mode mode, enum window_priority priority, - const char *class, const char *title, const char *exe) -{ - HWND parent; - bool use_findwindowex = false; - - HWND window = first_window(mode, &parent, &use_findwindowex); - HWND best_window = NULL; - int best_rating = 0x7FFFFFFF; - - if (!class) - return NULL; - - bool generic_class = is_generic_class(class); - - while (window) { - int rating = window_rating(window, priority, class, title, exe, - generic_class); - if (rating < best_rating) { - best_rating = rating; - best_window = window; - if (rating == 0) - break; - } - - window = next_window(window, mode, &parent, use_findwindowex); - } - - return best_window; -} - -struct top_level_enum_data { - enum window_search_mode mode; - enum window_priority priority; - const char *class; - const char *title; - const char *exe; - bool generic_class; - HWND best_window; - int best_rating; -}; - -BOOL CALLBACK enum_windows_proc(HWND window, LPARAM lParam) -{ - struct top_level_enum_data *data = (struct top_level_enum_data *)lParam; - - if (!check_window_valid(window, data->mode)) - return TRUE; - - int cloaked; - if (SUCCEEDED(DwmGetWindowAttribute(window, DWMWA_CLOAKED, &cloaked, - sizeof(cloaked))) && - cloaked) - return TRUE; - - const int rating = window_rating(window, data->priority, data->class, - data->title, data->exe, - data->generic_class); - if (rating < data->best_rating) { - data->best_rating = rating; - data->best_window = window; - } - - return rating > 0; -} - -HWND find_window_top_level(enum window_search_mode mode, - enum window_priority priority, const char *class, - const char *title, const char *exe) -{ - if (!class) - return NULL; - - struct top_level_enum_data data; - data.mode = mode; - data.priority = priority; - data.class = class; - data.title = title; - data.exe = exe; - data.generic_class = is_generic_class(class); - data.best_window = NULL; - data.best_rating = 0x7FFFFFFF; - EnumWindows(enum_windows_proc, (LPARAM)&data); - return data.best_window; -} +} \ No newline at end of file diff --git a/src/window-helpers.h b/src/window-helpers.h index b90fc9b..6ffc4b1 100644 --- a/src/window-helpers.h +++ b/src/window-helpers.h @@ -16,6 +16,17 @@ enum window_search_mode { EXCLUDE_MINIMIZED, }; +typedef struct window_info { + char *executable; + char *class; + char *title; +} window_info_t; + +extern void window_info_destroy(window_info_t *info); +extern bool window_info_cmp(window_info_t *info_a, window_info_t *info_b); +extern HWND window_info_get_window(window_info_t *info, + enum window_priority priority); + extern bool get_window_exe(struct dstr *name, HWND window); extern void get_window_title(struct dstr *name, HWND hwnd); extern void get_window_class(struct dstr *class, HWND hwnd); @@ -31,14 +42,4 @@ typedef bool (*add_window_cb)(const char *title, const char *class, extern void fill_window_list(obs_property_t *p, enum window_search_mode mode, add_window_cb callback); -extern void build_window_strings(const char *str, char **class, char **title, - char **exe); - -extern HWND find_window(enum window_search_mode mode, - enum window_priority priority, const char *class, - const char *title, const char *exe); - -extern HWND find_window_top_level(enum window_search_mode mode, - enum window_priority priority, - const char *class, const char *title, - const char *exe); +extern void build_window_strings(const char *str, window_info_t *info);