From ce3edda45e505270d4d289e4f07d5de23e2a5d78 Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sun, 29 Aug 2021 01:31:00 -0700 Subject: [PATCH] win-wasapi: Add support for capturing a process --- plugins/win-wasapi/data/locale/en-US.ini | 4 + plugins/win-wasapi/plugin-main.cpp | 14 + plugins/win-wasapi/win-wasapi.cpp | 333 +++++++++++++++++++---- 3 files changed, 304 insertions(+), 47 deletions(-) diff --git a/plugins/win-wasapi/data/locale/en-US.ini b/plugins/win-wasapi/data/locale/en-US.ini index fe4453cc976041..45737a0d62719d 100644 --- a/plugins/win-wasapi/data/locale/en-US.ini +++ b/plugins/win-wasapi/data/locale/en-US.ini @@ -3,3 +3,7 @@ AudioOutput="Audio Output Capture" Device="Device" Default="Default" UseDeviceTiming="Use Device Timestamps" +Method="Method" +Method.Device="Device" +Method.Process="Process (Windows 10 2004 and up)" +ProcessId="Process ID" diff --git a/plugins/win-wasapi/plugin-main.cpp b/plugins/win-wasapi/plugin-main.cpp index ddf255c41ce827..92e7207dc389c5 100644 --- a/plugins/win-wasapi/plugin-main.cpp +++ b/plugins/win-wasapi/plugin-main.cpp @@ -1,5 +1,7 @@ #include +#include + OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("win-wasapi", "en-US") MODULE_EXPORT const char *obs_module_description(void) @@ -10,8 +12,20 @@ MODULE_EXPORT const char *obs_module_description(void) void RegisterWASAPIInput(); void RegisterWASAPIOutput(); +bool process_filter_supported; + bool obs_module_load(void) { + /* MS says 20348, but process filtering seems to work earlier */ + struct win_version_info ver; + get_win_ver(&ver); + struct win_version_info minimum; + minimum.major = 10; + minimum.minor = 0; + minimum.build = 19041; + minimum.revis = 0; + process_filter_supported = win_version_compare(&ver, &minimum) >= 0; + RegisterWASAPIInput(); RegisterWASAPIOutput(); return true; diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 268e82e1b76c22..3fc1c885008244 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -10,31 +10,57 @@ #include #include +#include +#include + #include using namespace std; #define OPT_DEVICE_ID "device_id" #define OPT_USE_DEVICE_TIMING "use_device_timing" +#define OPT_METHOD obs_module_text("method") +#define OPT_METHOD_DEVICE obs_module_text("method.device") +#define OPT_METHOD_PROCESS obs_module_text("method.process") +#define OPT_PROCESS_ID obs_module_text("process_id") static void GetWASAPIDefaults(obs_data_t *settings); #define OBS_KSAUDIO_SPEAKER_4POINT1 \ (KSAUDIO_SPEAKER_SURROUND | SPEAKER_LOW_FREQUENCY) -class WASAPISource { +typedef HRESULT(STDAPICALLTYPE *PFN_ActivateAudioInterfaceAsync)( + LPCWSTR, REFIID, PROPVARIANT *, + IActivateAudioInterfaceCompletionHandler *, + IActivateAudioInterfaceAsyncOperation **); + +enum audio_capture_method { + METHOD_DEVICE, + METHOD_PROCESS, +}; + +class WASAPISource + : public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + Microsoft::WRL::FtmBase, + IActivateAudioInterfaceCompletionHandler> { + ComPtr notify; + ComPtr enumerator; ComPtr device; ComPtr client; - ComPtr capture; ComPtr render; - ComPtr enumerator; - ComPtr notify; + ComPtr capture; obs_source_t *source; wstring default_id; string device_id; string device_name; string device_sample = "-"; + int method; + SRWLOCK update_mutex; + HMODULE hModule = NULL; + PFN_ActivateAudioInterfaceAsync activate_audio_interface_async = NULL; + DWORD process_id = 0; uint64_t lastNotifyTime = 0; bool isInputDevice; bool useDeviceTiming = false; @@ -47,6 +73,8 @@ class WASAPISource { bool active = false; WinHandle captureThread; + HRESULT activationResult; + WinHandle activationSignal; WinHandle stopSignal; WinHandle receiveSignal; @@ -63,23 +91,29 @@ class WASAPISource { inline void Stop(); void Reconnect(); - bool InitDevice(); + void InitDevice(); void InitName(); void InitClient(); void InitRender(); - void InitFormat(WAVEFORMATEX *wfex); + void InitFormat(const WAVEFORMATEX *wfex); void InitCapture(); void Initialize(); bool TryInitialize(); + void UnwindInitialize(); void UpdateSettings(obs_data_t *settings); + virtual HRESULT STDMETHODCALLTYPE ActivateCompleted( + IActivateAudioInterfaceAsyncOperation *activateOperation) + override final; + public: WASAPISource(obs_data_t *settings, obs_source_t *source_, bool input); inline ~WASAPISource(); void Update(obs_data_t *settings); + void UpdateSettingsVisibility(obs_properties_t *props); void SetDefaultDevice(EDataFlow flow, ERole role, LPCWSTR id); }; @@ -139,8 +173,21 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, bool input) : source(source_), isInputDevice(input) { + InitializeSRWLock(&update_mutex); + + hModule = LoadLibrary(L"Mmdevapi"); + if (hModule) { + activate_audio_interface_async = + (PFN_ActivateAudioInterfaceAsync)GetProcAddress( + hModule, "ActivateAudioInterfaceAsync"); + } + UpdateSettings(settings); + activationSignal = CreateEvent(nullptr, false, false, nullptr); + if (!activationSignal.Valid()) + throw "Could not create receive signal"; + stopSignal = CreateEvent(nullptr, true, false, nullptr); if (!stopSignal.Valid()) throw "Could not create stop signal"; @@ -149,6 +196,10 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, if (!receiveSignal.Valid()) throw "Could not create receive signal"; + notify = new WASAPINotify(this); + if (!notify) + throw "Could not create WASAPINotify"; + Start(); } @@ -181,21 +232,45 @@ inline void WASAPISource::Stop() inline WASAPISource::~WASAPISource() { - enumerator->UnregisterEndpointNotificationCallback(notify); + if (enumerator) + enumerator->UnregisterEndpointNotificationCallback(notify); + Stop(); + + FreeLibrary(hModule); } void WASAPISource::UpdateSettings(obs_data_t *settings) { + AcquireSRWLockExclusive(&update_mutex); + device_id = obs_data_get_string(settings, OPT_DEVICE_ID); useDeviceTiming = obs_data_get_bool(settings, OPT_USE_DEVICE_TIMING); isDefaultDevice = _strcmpi(device_id.c_str(), "default") == 0; + if (!isInputDevice) { + method = (audio_capture_method)obs_data_get_int(settings, + OPT_METHOD); + process_id = + atoi(obs_data_get_string(settings, OPT_PROCESS_ID)); + } + + ReleaseSRWLockExclusive(&update_mutex); } void WASAPISource::Update(obs_data_t *settings) { string newDevice = obs_data_get_string(settings, OPT_DEVICE_ID); - bool restart = newDevice.compare(device_id) != 0; + bool restart = (newDevice.compare(device_id) != 0); + if (!isInputDevice) { + audio_capture_method newMethod = + (audio_capture_method)obs_data_get_int(settings, + OPT_METHOD); + const DWORD newProcessId = + atoi(obs_data_get_string(settings, OPT_PROCESS_ID)); + restart = restart || (method != newMethod) || + ((method == METHOD_PROCESS) && + (process_id != newProcessId)); + } if (restart) Stop(); @@ -206,7 +281,21 @@ void WASAPISource::Update(obs_data_t *settings) Start(); } -bool WASAPISource::InitDevice() +void WASAPISource::UpdateSettingsVisibility(obs_properties_t *props) +{ + AcquireSRWLockExclusive(&update_mutex); + + if (!isInputDevice) { + const bool use_process_id = method == METHOD_PROCESS; + + obs_property_t *p = obs_properties_get(props, OPT_PROCESS_ID); + obs_property_set_visible(p, use_process_id); + } + + ReleaseSRWLockExclusive(&update_mutex); +} + +void WASAPISource::InitDevice() { HRESULT res; @@ -216,7 +305,7 @@ bool WASAPISource::InitDevice() isInputDevice ? eCommunications : eConsole, device.Assign()); if (FAILED(res)) - return false; + throw HRError("Failed GetDefaultAudioEndpoint", res); CoTaskMemPtr id; res = device->GetId(&id); @@ -229,37 +318,118 @@ bool WASAPISource::InitDevice() res = enumerator->GetDevice(w_id, device.Assign()); bfree(w_id); - } - return SUCCEEDED(res); + if (FAILED(res)) + throw HRError("Failed to enumerate device", res); + } } #define BUFFER_TIME_100NS (5 * 10000000) +static DWORD GetSpeakerChannelMask(speaker_layout layout) +{ + switch (layout) { + case SPEAKERS_STEREO: + return KSAUDIO_SPEAKER_STEREO; + case SPEAKERS_2POINT1: + return KSAUDIO_SPEAKER_2POINT1; + case SPEAKERS_4POINT0: + return KSAUDIO_SPEAKER_SURROUND; + case SPEAKERS_4POINT1: + return OBS_KSAUDIO_SPEAKER_4POINT1; + case SPEAKERS_5POINT1: + return KSAUDIO_SPEAKER_5POINT1_SURROUND; + case SPEAKERS_7POINT1: + return KSAUDIO_SPEAKER_7POINT1_SURROUND; + } + + return (DWORD)layout; +} + void WASAPISource::InitClient() { + WAVEFORMATEXTENSIBLE wfextensible; CoTaskMemPtr wfex; + const WAVEFORMATEX *pFormat; HRESULT res; - DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK; - res = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, - (void **)client.Assign()); - if (FAILED(res)) - throw HRError("Failed to activate client context", res); + if (!isInputDevice && (method == METHOD_PROCESS)) { + if (activate_audio_interface_async == NULL) + throw "ActivateAudioInterfaceAsync is not available"; + + struct obs_audio_info oai; + obs_get_audio_info(&oai); + + const WORD nChannels = (WORD)get_audio_channels(oai.speakers); + const DWORD nSamplesPerSec = oai.samples_per_sec; + constexpr WORD wBitsPerSample = 32; + const WORD nBlockAlign = nChannels * wBitsPerSample / 8; + + WAVEFORMATEX &format = wfextensible.Format; + format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + format.nChannels = nChannels; + format.nSamplesPerSec = nSamplesPerSec; + format.nAvgBytesPerSec = nSamplesPerSec * nBlockAlign; + format.nBlockAlign = nBlockAlign; + format.wBitsPerSample = wBitsPerSample; + format.cbSize = sizeof(wfextensible) - sizeof(format); + wfextensible.Samples.wValidBitsPerSample = wBitsPerSample; + wfextensible.dwChannelMask = + GetSpeakerChannelMask(oai.speakers); + wfextensible.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + + AUDIOCLIENT_ACTIVATION_PARAMS audioclientActivationParams; + audioclientActivationParams.ActivationType = + AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK; + audioclientActivationParams.ProcessLoopbackParams + .TargetProcessId = process_id; + audioclientActivationParams.ProcessLoopbackParams + .ProcessLoopbackMode = + PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE; + PROPVARIANT activateParams{}; + activateParams.vt = VT_BLOB; + activateParams.blob.cbSize = + sizeof(audioclientActivationParams); + activateParams.blob.pBlobData = + reinterpret_cast(&audioclientActivationParams); + + ComPtr asyncOp; + res = activate_audio_interface_async( + VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK, + __uuidof(IAudioClient), &activateParams, this, + &asyncOp); + if (FAILED(res)) + throw HRError("Failed to get activate audio client", + res); - res = client->GetMixFormat(&wfex); - if (FAILED(res)) - throw HRError("Failed to get mix format", res); + WaitForSingleObject(activationSignal, INFINITE); + res = activationResult; + if (FAILED(res)) + throw HRError("Async activation failed", res); - InitFormat(wfex); + pFormat = &format; + } else { + res = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, + nullptr, (void **)client.Assign()); + if (FAILED(res)) + throw HRError("Failed to activate client context", res); + + res = client->GetMixFormat(&wfex); + if (FAILED(res)) + throw HRError("Failed to get mix format", res); + pFormat = wfex.Get(); + } + + InitFormat(pFormat); + + DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK; if (!isInputDevice) flags |= AUDCLNT_STREAMFLAGS_LOOPBACK; - res = client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, - BUFFER_TIME_100NS, 0, wfex, nullptr); + BUFFER_TIME_100NS, 0, pFormat, nullptr); if (FAILED(res)) - throw HRError("Failed to get initialize audio client", res); + throw HRError("Failed to initialize audio client", res); } void WASAPISource::InitRender() @@ -282,7 +452,7 @@ void WASAPISource::InitRender() res = client->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, BUFFER_TIME_100NS, 0, wfex, nullptr); if (FAILED(res)) - throw HRError("Failed to get initialize audio client", res); + throw HRError("Failed to initialize audio client", res); /* Silent loopback fix. Prevents audio stream from stopping and */ /* messing up timestamps and other weird glitches during silence */ @@ -292,8 +462,7 @@ void WASAPISource::InitRender() if (FAILED(res)) throw HRError("Failed to get buffer size", res); - res = client->GetService(__uuidof(IAudioRenderClient), - (void **)render.Assign()); + res = client->GetService(IID_PPV_ARGS(render.Assign())); if (FAILED(res)) throw HRError("Failed to get render client", res); @@ -301,7 +470,7 @@ void WASAPISource::InitRender() if (FAILED(res)) throw HRError("Failed to get buffer", res); - memset(buffer, 0, frames * wfex->nBlockAlign); + memset(buffer, 0, (size_t)frames * (size_t)wfex->nBlockAlign); render->ReleaseBuffer(frames, 0); } @@ -324,7 +493,7 @@ static speaker_layout ConvertSpeakerLayout(DWORD layout, WORD channels) return (speaker_layout)channels; } -void WASAPISource::InitFormat(WAVEFORMATEX *wfex) +void WASAPISource::InitFormat(const WAVEFORMATEX *wfex) { DWORD layout = 0; @@ -333,16 +502,15 @@ void WASAPISource::InitFormat(WAVEFORMATEX *wfex) layout = ext->dwChannelMask; } - /* WASAPI is always float */ sampleRate = wfex->nSamplesPerSec; - format = AUDIO_FORMAT_FLOAT; + format = wfex->wFormatTag == WAVE_FORMAT_PCM ? AUDIO_FORMAT_16BIT + : AUDIO_FORMAT_FLOAT; speakers = ConvertSpeakerLayout(layout, wfex->nChannels); } void WASAPISource::InitCapture() { - HRESULT res = client->GetService(__uuidof(IAudioCaptureClient), - (void **)capture.Assign()); + HRESULT res = client->GetService(IID_PPV_ARGS(capture.Assign())); if (FAILED(res)) throw HRError("Failed to create capture context", res); @@ -372,15 +540,15 @@ void WASAPISource::Initialize() if (FAILED(res)) throw HRError("Failed to create enumerator", res); - if (!InitDevice()) - return; + res = enumerator->RegisterEndpointNotificationCallback(notify); + if (FAILED(res)) { + enumerator.Clear(); + throw HRError("Failed to register endpoint callback", res); + } - device_name = GetDeviceName(device); + InitDevice(); - if (!notify) { - notify = new WASAPINotify(this); - enumerator->RegisterEndpointNotificationCallback(notify); - } + device_name = GetDeviceName(device); HRESULT resSample; IPropertyStore *store = nullptr; @@ -414,6 +582,8 @@ bool WASAPISource::TryInitialize() Initialize(); } catch (HRError &error) { + UnwindInitialize(); + if (previouslyFailed) return active; @@ -423,6 +593,8 @@ bool WASAPISource::TryInitialize() error.str, error.hr); } catch (const char *error) { + UnwindInitialize(); + if (previouslyFailed) return active; @@ -436,6 +608,18 @@ bool WASAPISource::TryInitialize() return active; } +void WASAPISource::UnwindInitialize() +{ + capture.Clear(); + render.Clear(); + client.Clear(); + device.Clear(); + if (enumerator) { + enumerator->UnregisterEndpointNotificationCallback(notify); + enumerator.Clear(); + } +} + void WASAPISource::Reconnect() { reconnecting = true; @@ -618,6 +802,19 @@ void WASAPISource::SetDefaultDevice(EDataFlow flow, ERole role, LPCWSTR id) lastNotifyTime = t; } +HRESULT STDMETHODCALLTYPE WASAPISource::ActivateCompleted( + IActivateAudioInterfaceAsyncOperation *activateOperation) +{ + HRESULT hr, hr_activate; + hr = activateOperation->GetActivateResult(&hr_activate, + (IUnknown **)client.Assign()); + hr = SUCCEEDED(hr) ? hr_activate : hr; + activationResult = hr; + + SetEvent(activationSignal); + return hr; +} + /* ------------------------------------------------------------------------- */ static const char *GetWASAPIInputName(void *) @@ -640,13 +837,17 @@ static void GetWASAPIDefaultsOutput(obs_data_t *settings) { obs_data_set_default_string(settings, OPT_DEVICE_ID, "default"); obs_data_set_default_bool(settings, OPT_USE_DEVICE_TIMING, true); + obs_data_set_default_int(settings, OPT_METHOD, METHOD_DEVICE); + obs_data_set_default_string(settings, OPT_PROCESS_ID, "0"); } static void *CreateWASAPISource(obs_data_t *settings, obs_source_t *source, bool input) { try { - return new WASAPISource(settings, source, input); + return Microsoft::WRL::Make(settings, source, + input) + .Detach(); } catch (const char *error) { blog(LOG_ERROR, "[CreateWASAPISource] %s", error); } @@ -666,7 +867,7 @@ static void *CreateWASAPIOutput(obs_data_t *settings, obs_source_t *source) static void DestroyWASAPISource(void *obj) { - delete static_cast(obj); + static_cast(obj)->Release(); } static void UpdateWASAPISource(void *obj, obs_data_t *settings) @@ -674,9 +875,25 @@ static void UpdateWASAPISource(void *obj, obs_data_t *settings) static_cast(obj)->Update(settings); } -static obs_properties_t *GetWASAPIProperties(bool input) +static bool UpdateWASAPIMethod(obs_properties_t *props, obs_property_t *, + obs_data_t *settings) +{ + WASAPISource *source = (WASAPISource *)obs_properties_get_param(props); + if (!source) + return false; + + source->Update(settings); + source->UpdateSettingsVisibility(props); + + return true; +} + +extern bool process_filter_supported; + +static obs_properties_t *GetWASAPIProperties(void *data, bool input) { obs_properties_t *props = obs_properties_create(); + obs_properties_set_param(props, data, NULL); vector devices; obs_property_t *device_prop = obs_properties_add_list( @@ -695,20 +912,42 @@ static obs_properties_t *GetWASAPIProperties(bool input) device.id.c_str()); } + if (!input) { + obs_property_t *method_prop = obs_properties_add_list( + props, OPT_METHOD, obs_module_text("Method"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(method_prop, + obs_module_text("Method.Device"), + METHOD_DEVICE); + obs_property_list_add_int(method_prop, + obs_module_text("Method.Process"), + METHOD_PROCESS); + obs_property_list_item_disable(method_prop, 1, + !process_filter_supported); + obs_property_set_modified_callback(method_prop, + UpdateWASAPIMethod); + } + obs_properties_add_bool(props, OPT_USE_DEVICE_TIMING, obs_module_text("UseDeviceTiming")); + if (!input) { + obs_properties_add_text(props, OPT_PROCESS_ID, + obs_module_text("ProcessId"), + OBS_TEXT_DEFAULT); + } + return props; } -static obs_properties_t *GetWASAPIPropertiesInput(void *) +static obs_properties_t *GetWASAPIPropertiesInput(void *data) { - return GetWASAPIProperties(true); + return GetWASAPIProperties(data, true); } -static obs_properties_t *GetWASAPIPropertiesOutput(void *) +static obs_properties_t *GetWASAPIPropertiesOutput(void *data) { - return GetWASAPIProperties(false); + return GetWASAPIProperties(data, false); } void RegisterWASAPIInput()