Browse Source

win-wasapi: Remove bools and persist threads

This simplifies synchronization, and fixes several races.
jpark37 4 years ago
parent
commit
a995c305b7
1 changed files with 215 additions and 133 deletions
  1. 215 133
      plugins/win-wasapi/win-wasapi.cpp

+ 215 - 133
plugins/win-wasapi/win-wasapi.cpp

@@ -10,8 +10,8 @@
 #include <util/threading.h>
 #include <util/util_uint64.h>
 
+#include <atomic>
 #include <cinttypes>
-#include <thread>
 
 using namespace std;
 
@@ -35,18 +35,21 @@ class WASAPISource {
 	string device_name;
 	uint64_t lastNotifyTime = 0;
 	bool isInputDevice;
-	bool useDeviceTiming = false;
+	std::atomic<bool> useDeviceTiming = false;
 	bool isDefaultDevice = false;
 
-	bool reconnecting = false;
 	bool previouslyFailed = false;
 	WinHandle reconnectThread;
 
-	bool active = false;
 	WinHandle captureThread;
-
+	WinHandle idleSignal;
 	WinHandle stopSignal;
 	WinHandle receiveSignal;
+	WinHandle restartSignal;
+	WinHandle exitSignal;
+	WinHandle initSignal;
+	DWORD reconnectDuration = 0;
+	WinHandle reconnectSignal;
 
 	speaker_layout speakers;
 	audio_format format;
@@ -59,7 +62,6 @@ class WASAPISource {
 
 	void Start();
 	void Stop();
-	void Reconnect();
 
 	static ComPtr<IMMDevice> InitDevice(IMMDeviceEnumerator *enumerator,
 					    bool isDefaultDevice,
@@ -149,6 +151,10 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_,
 {
 	UpdateSettings(settings);
 
+	idleSignal = CreateEvent(nullptr, true, false, nullptr);
+	if (!idleSignal.Valid())
+		throw "Could not create idle signal";
+
 	stopSignal = CreateEvent(nullptr, true, false, nullptr);
 	if (!stopSignal.Valid())
 		throw "Could not create stop signal";
@@ -157,49 +163,69 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_,
 	if (!receiveSignal.Valid())
 		throw "Could not create receive signal";
 
+	restartSignal = CreateEvent(nullptr, true, false, nullptr);
+	if (!restartSignal.Valid())
+		throw "Could not create restart signal";
+
+	exitSignal = CreateEvent(nullptr, true, false, nullptr);
+	if (!exitSignal.Valid())
+		throw "Could not create exit signal";
+
+	initSignal = CreateEvent(nullptr, false, false, nullptr);
+	if (!initSignal.Valid())
+		throw "Could not create init signal";
+
+	reconnectSignal = CreateEvent(nullptr, false, false, nullptr);
+	if (!reconnectSignal.Valid())
+		throw "Could not create reconnect signal";
+
+	reconnectThread = CreateThread(
+		nullptr, 0, WASAPISource::ReconnectThread, this, 0, nullptr);
+	if (!reconnectThread.Valid())
+		throw "Failed to create reconnect thread";
+
 	notify = new WASAPINotify(this);
 	if (!notify)
 		throw "Could not create WASAPINotify";
 
-	HRESULT res = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr,
-				       CLSCTX_ALL,
-				       IID_PPV_ARGS(enumerator.Assign()));
-	if (FAILED(res))
-		throw HRError("Failed to create enumerator", res);
+	HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr,
+				      CLSCTX_ALL,
+				      IID_PPV_ARGS(enumerator.Assign()));
+	if (FAILED(hr))
+		throw HRError("Failed to create enumerator", hr);
 
-	res = enumerator->RegisterEndpointNotificationCallback(notify);
-	if (FAILED(res))
-		throw HRError("Failed to register endpoint callback", res);
+	hr = enumerator->RegisterEndpointNotificationCallback(notify);
+	if (FAILED(hr))
+		throw HRError("Failed to register endpoint callback", hr);
+
+	captureThread = CreateThread(nullptr, 0, WASAPISource::CaptureThread,
+				     this, 0, nullptr);
+	if (!captureThread.Valid()) {
+		enumerator->UnregisterEndpointNotificationCallback(notify);
+		throw "Failed to create capture thread";
+	}
 
 	Start();
 }
 
 void WASAPISource::Start()
 {
-	if (!TryInitialize()) {
-		blog(LOG_INFO, "WASAPI: Device '%s' failed to start",
-		     device_id.c_str());
-		Reconnect();
-	}
+	SetEvent(initSignal);
 }
 
 void WASAPISource::Stop()
 {
 	SetEvent(stopSignal);
 
-	if (active) {
-		blog(LOG_INFO, "WASAPI: Device '%s' Terminated",
-		     device_name.c_str());
-		WaitForSingleObject(captureThread, INFINITE);
-	}
+	blog(LOG_INFO, "WASAPI: Device '%s' Terminated", device_name.c_str());
 
-	if (reconnecting)
-		WaitForSingleObject(reconnectThread, INFINITE);
+	WaitForSingleObject(idleSignal, INFINITE);
 
-	ResetEvent(stopSignal);
+	SetEvent(exitSignal);
 
-	capture.Clear();
-	client.Clear();
+	WaitForSingleObject(reconnectThread, INFINITE);
+
+	WaitForSingleObject(captureThread, INFINITE);
 }
 
 WASAPISource::~WASAPISource()
@@ -217,16 +243,13 @@ void WASAPISource::UpdateSettings(obs_data_t *settings)
 
 void WASAPISource::Update(obs_data_t *settings)
 {
-	string newDevice = obs_data_get_string(settings, OPT_DEVICE_ID);
-	bool restart = newDevice.compare(device_id) != 0;
-
-	if (restart)
-		Stop();
+	const string newDevice = obs_data_get_string(settings, OPT_DEVICE_ID);
+	const bool restart = newDevice.compare(device_id) != 0;
 
 	UpdateSettings(settings);
 
 	if (restart)
-		Start();
+		SetEvent(restartSignal);
 }
 
 ComPtr<IMMDevice> WASAPISource::InitDevice(IMMDeviceEnumerator *enumerator,
@@ -407,6 +430,8 @@ void WASAPISource::Initialize()
 
 	device_name = GetDeviceName(device);
 
+	ResetEvent(receiveSignal);
+
 	ComPtr<IAudioClient> temp_client =
 		InitClient(device, isInputDevice, speakers, format, sampleRate);
 	if (!isInputDevice)
@@ -417,93 +442,68 @@ void WASAPISource::Initialize()
 	client = std::move(temp_client);
 	capture = std::move(temp_capture);
 
-	captureThread = CreateThread(nullptr, 0, WASAPISource::CaptureThread,
-				     this, 0, nullptr);
-	if (!captureThread.Valid()) {
-		capture.Clear();
-		client.Clear();
-		throw "Failed to create capture thread";
-	}
-
-	active = true;
-
 	blog(LOG_INFO, "WASAPI: Device '%s' [%" PRIu32 " Hz] initialized",
 	     device_name.c_str(), sampleRate);
 }
 
 bool WASAPISource::TryInitialize()
 {
+	bool success = false;
 	try {
 		Initialize();
+		success = true;
 
 	} catch (HRError &error) {
-		if (previouslyFailed)
-			return active;
-
-		blog(LOG_WARNING, "[WASAPISource::TryInitialize]:[%s] %s: %lX",
-		     device_name.empty() ? device_id.c_str()
-					 : device_name.c_str(),
-		     error.str, error.hr);
-
+		if (!previouslyFailed) {
+			blog(LOG_WARNING,
+			     "[WASAPISource::TryInitialize]:[%s] %s: %lX",
+			     device_name.empty() ? device_id.c_str()
+						 : device_name.c_str(),
+			     error.str, error.hr);
+		}
 	} catch (const char *error) {
-		if (previouslyFailed)
-			return active;
-
-		blog(LOG_WARNING, "[WASAPISource::TryInitialize]:[%s] %s",
-		     device_name.empty() ? device_id.c_str()
-					 : device_name.c_str(),
-		     error);
+		if (!previouslyFailed) {
+			blog(LOG_WARNING,
+			     "[WASAPISource::TryInitialize]:[%s] %s",
+			     device_name.empty() ? device_id.c_str()
+						 : device_name.c_str(),
+			     error);
+		}
 	}
 
-	previouslyFailed = !active;
-	return active;
+	previouslyFailed = !success;
+	return success;
 }
 
-void WASAPISource::Reconnect()
-{
-	reconnecting = true;
-	reconnectThread = CreateThread(
-		nullptr, 0, WASAPISource::ReconnectThread, this, 0, nullptr);
-
-	if (!reconnectThread.Valid())
-		blog(LOG_WARNING,
-		     "[WASAPISource::Reconnect] "
-		     "Failed to initialize reconnect thread: %lu",
-		     GetLastError());
-}
-
-static inline bool WaitForSignal(HANDLE handle, DWORD time)
-{
-	return WaitForSingleObject(handle, time) != WAIT_TIMEOUT;
-}
-
-#define RECONNECT_INTERVAL 3000
-
 DWORD WINAPI WASAPISource::ReconnectThread(LPVOID param)
 {
-	WASAPISource *source = (WASAPISource *)param;
-
 	os_set_thread_name("win-wasapi: reconnect thread");
 
-	const HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);
-	const bool com_initialized = SUCCEEDED(hr);
-	if (!com_initialized) {
-		blog(LOG_ERROR,
-		     "[WASAPISource::ReconnectThread]"
-		     " CoInitializeEx failed: 0x%08X",
-		     hr);
-	}
+	WASAPISource *source = (WASAPISource *)param;
 
-	while (!WaitForSignal(source->stopSignal, RECONNECT_INTERVAL)) {
-		if (source->TryInitialize())
+	const HANDLE sigs[] = {
+		source->exitSignal,
+		source->reconnectSignal,
+	};
+
+	bool exit = false;
+	while (!exit) {
+		const DWORD ret = WaitForMultipleObjects(_countof(sigs), sigs,
+							 false, INFINITE);
+		switch (ret) {
+		case WAIT_OBJECT_0:
+			exit = true;
 			break;
+		default:
+			assert(ret == (WAIT_OBJECT_0 + 1));
+			if (source->reconnectDuration > 0) {
+				WaitForSingleObject(source->stopSignal,
+						    source->reconnectDuration);
+			}
+			source->Start();
+		}
 	}
 
-	if (com_initialized)
-		CoUninitialize();
-
-	source->reconnectThread = nullptr;
-	source->reconnecting = false;
 	return 0;
 }
 
@@ -518,7 +518,6 @@ bool WASAPISource::ProcessCaptureData()
 
 	while (true) {
 		res = capture->GetNextPacketSize(&captureSize);
-
 		if (FAILED(res)) {
 			if (res != AUDCLNT_E_DEVICE_INVALIDATED)
 				blog(LOG_WARNING,
@@ -563,45 +562,131 @@ bool WASAPISource::ProcessCaptureData()
 	return true;
 }
 
-static inline bool WaitForCaptureSignal(DWORD numSignals, const HANDLE *signals,
-					DWORD duration)
-{
-	DWORD ret;
-	ret = WaitForMultipleObjects(numSignals, signals, false, duration);
-
-	return ret == WAIT_OBJECT_0 || ret == WAIT_TIMEOUT;
-}
+#define RECONNECT_INTERVAL 3000
 
 DWORD WINAPI WASAPISource::CaptureThread(LPVOID param)
 {
-	WASAPISource *source = (WASAPISource *)param;
-	bool reconnect = false;
-
-	/* Output devices don't signal, so just make it check every 10 ms */
-	DWORD dur = source->isInputDevice ? RECONNECT_INTERVAL : 10;
-
-	HANDLE sigs[2] = {source->receiveSignal, source->stopSignal};
-
 	os_set_thread_name("win-wasapi: capture thread");
 
-	while (WaitForCaptureSignal(2, sigs, dur)) {
-		if (!source->ProcessCaptureData()) {
-			reconnect = true;
-			break;
-		}
+	const HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);
+	const bool com_initialized = SUCCEEDED(hr);
+	if (!com_initialized) {
+		blog(LOG_ERROR,
+		     "[WASAPISource::CaptureThread]"
+		     " CoInitializeEx failed: 0x%08X",
+		     hr);
 	}
 
-	source->client->Stop();
+	WASAPISource *source = (WASAPISource *)param;
 
-	source->captureThread = nullptr;
-	source->active = false;
+	const HANDLE inactive_sigs[] = {
+		source->exitSignal,
+		source->stopSignal,
+		source->initSignal,
+	};
+
+	const HANDLE active_sigs[] = {
+		source->exitSignal,
+		source->stopSignal,
+		source->receiveSignal,
+		source->restartSignal,
+	};
+
+	DWORD sig_count = _countof(inactive_sigs);
+	const HANDLE *sigs = inactive_sigs;
+
+	bool exit = false;
+	while (!exit) {
+		bool idle = false;
+		bool stop = false;
+		bool reconnect = false;
+		do {
+			/* Windows 7 does not seem to wake up for LOOPBACK */
+			const DWORD dwMilliseconds = ((sigs == active_sigs) &&
+						      !source->isInputDevice)
+							     ? 10
+							     : INFINITE;
+
+			const DWORD ret = WaitForMultipleObjects(
+				sig_count, sigs, false, dwMilliseconds);
+			switch (ret) {
+			case WAIT_OBJECT_0: {
+				exit = true;
+				stop = true;
+				idle = true;
+				break;
+			}
+
+			case WAIT_OBJECT_0 + 1:
+				stop = true;
+				idle = true;
+				break;
+
+			case WAIT_OBJECT_0 + 2:
+			case WAIT_TIMEOUT:
+				if (sigs == inactive_sigs) {
+					assert(ret != WAIT_TIMEOUT);
+
+					if (source->TryInitialize()) {
+						sig_count =
+							_countof(active_sigs);
+						sigs = active_sigs;
+					} else {
+						blog(LOG_INFO,
+						     "WASAPI: Device '%s' failed to start",
+						     source->device_id.c_str());
+						stop = true;
+						reconnect = true;
+						source->reconnectDuration =
+							RECONNECT_INTERVAL;
+					}
+				} else {
+					stop = !source->ProcessCaptureData();
+					if (stop) {
+						blog(LOG_INFO,
+						     "Device '%s' invalidated.  Retrying",
+						     source->device_name
+							     .c_str());
+						stop = true;
+						reconnect = true;
+						source->reconnectDuration =
+							RECONNECT_INTERVAL;
+					}
+				}
+				break;
+
+			default:
+				assert(sigs == active_sigs);
+				assert(ret == WAIT_OBJECT_0 + 3);
+				stop = true;
+				reconnect = true;
+				source->reconnectDuration = 0;
+				ResetEvent(source->restartSignal);
+			}
+		} while (!stop);
+
+		sig_count = _countof(inactive_sigs);
+		sigs = inactive_sigs;
+
+		if (source->client) {
+			source->client->Stop();
+
+			source->capture.Clear();
+			source->client.Clear();
+		}
 
-	if (reconnect) {
-		blog(LOG_INFO, "Device '%s' invalidated.  Retrying",
-		     source->device_name.c_str());
-		source->Reconnect();
+		if (idle) {
+			SetEvent(source->idleSignal);
+		} else if (reconnect) {
+			blog(LOG_INFO, "Device '%s' invalidated.  Retrying",
+			     source->device_name.c_str());
+			SetEvent(source->reconnectSignal);
+		}
 	}
 
+	if (com_initialized)
+		CoUninitialize();
+
 	return 0;
 }
 
@@ -626,12 +711,9 @@ void WASAPISource::SetDefaultDevice(EDataFlow flow, ERole role, LPCWSTR id)
 	if (t - lastNotifyTime < 300000000)
 		return;
 
-	std::thread([this]() {
-		Stop();
-		Start();
-	}).detach();
-
 	lastNotifyTime = t;
+
+	SetEvent(restartSignal);
 }
 
 /* ------------------------------------------------------------------------- */