diff --git a/plugins/win-dshow/win-dshow.cpp b/plugins/win-dshow/win-dshow.cpp
index 6f9ee0872ff224ba4c9f46be9663fec29cc98a5d..929d51ed799125b67614cb9eb67c575502ece787 100644
--- a/plugins/win-dshow/win-dshow.cpp
+++ b/plugins/win-dshow/win-dshow.cpp
@@ -5,6 +5,7 @@
 #include <util/dstr.hpp>
 #include <util/util.hpp>
 #include <util/platform.h>
+#include <util/windows/WinHandle.hpp>
 #include "libdshowcapture/dshowcapture.hpp"
 #include "ffmpeg-decode.h"
 
@@ -87,10 +88,49 @@ public:
 	inline operator ffmpeg_decode*() {return &decode;}
 };
 
+class CriticalSection {
+	CRITICAL_SECTION mutex;
+
+public:
+	inline CriticalSection()  {InitializeCriticalSection(&mutex);}
+	inline ~CriticalSection() {DeleteCriticalSection(&mutex);}
+
+	inline operator CRITICAL_SECTION*() {return &mutex;}
+};
+
+class CriticalScope {
+	CriticalSection &mutex;
+
+	CriticalScope() = delete;
+	CriticalScope& operator=(CriticalScope &cs) = delete;
+
+public:
+	inline CriticalScope(CriticalSection &mutex_) : mutex(mutex_)
+	{
+		EnterCriticalSection(mutex);
+	}
+
+	inline ~CriticalScope()
+	{
+		LeaveCriticalSection(mutex);
+	}
+};
+
+enum class Action {
+	None,
+	Update,
+	Shutdown,
+	ConfigVideo,
+	ConfigAudio,
+	ConfigCrossbar1,
+	ConfigCrossbar2,
+};
+
+static DWORD CALLBACK DShowThread(LPVOID ptr);
+
 struct DShowInput {
 	obs_source_t source;
 	Device       device;
-	bool         comInitialized;
 	bool         deviceHasAudio;
 
 	Decoder      audio_decoder;
@@ -102,16 +142,51 @@ struct DShowInput {
 	obs_source_frame frame;
 	obs_source_audio audio;
 
+	WinHandle semaphore;
+	WinHandle thread;
+	CriticalSection mutex;
+	vector<Action> actions;
+
+	inline void QueueAction(Action action)
+	{
+		CriticalScope scope(mutex);
+		actions.push_back(action);
+		ReleaseSemaphore(semaphore, 1, nullptr);
+	}
+
 	inline DShowInput(obs_source_t source_)
 		: source         (source_),
-		  device         (InitGraph::False),
-		  comInitialized (false)
+		  device         (InitGraph::False)
 	{
 		memset(&audio, 0, sizeof(audio));
 		memset(&frame, 0, sizeof(frame));
 
 		av_log_set_level(AV_LOG_WARNING);
 		av_log_set_callback(ffmpeg_log);
+
+		semaphore = CreateSemaphore(nullptr, 0, 0x7FFFFFFF, nullptr);
+		if (!semaphore)
+			throw "Failed to create semaphore";
+
+		thread = CreateThread(nullptr, 0, DShowThread, this, 0,
+				nullptr);
+		if (!thread)
+			throw "Failed to create thread";
+
+		QueueAction(Action::Update);
+	}
+
+	inline ~DShowInput()
+	{
+		{
+			CriticalScope scope(mutex);
+			actions.resize(1);
+			actions[0] = Action::Shutdown;
+		}
+
+		ReleaseSemaphore(semaphore, 1, nullptr);
+
+		WaitForSingleObject(thread, INFINITE);
 	}
 
 	void OnEncodedVideoData(enum AVCodecID id,
@@ -129,8 +204,86 @@ struct DShowInput {
 	bool UpdateVideoConfig(obs_data_t settings);
 	bool UpdateAudioConfig(obs_data_t settings);
 	void Update(obs_data_t settings);
+
+	void DShowLoop();
 };
 
+static DWORD CALLBACK DShowThread(LPVOID ptr)
+{
+	DShowInput *dshowInput = (DShowInput*)ptr;
+
+	CoInitialize(nullptr);
+	dshowInput->DShowLoop();
+	CoUninitialize();
+	return 0;
+}
+
+static inline void ProcessMessages()
+{
+	MSG msg;
+	while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
+		TranslateMessage(&msg);
+		DispatchMessage(&msg);
+	}
+}
+
+/* Always keep directshow in a single thread for a given device */
+void DShowInput::DShowLoop()
+{
+	while (true) {
+		DWORD ret = MsgWaitForMultipleObjects(1, &semaphore, false,
+				INFINITE, QS_ALLINPUT);
+		if (ret == (WAIT_OBJECT_0 + 1)) {
+			ProcessMessages();
+			continue;
+		} else if (ret != WAIT_OBJECT_0) {
+			break;
+		}
+
+		Action action = Action::None;
+		{
+			CriticalScope scope(mutex);
+			if (actions.size()) {
+				action = actions.front();
+				actions.erase(actions.begin());
+			}
+		}
+
+		switch (action) {
+		case Action::Update:
+			{
+				obs_data_t settings;
+				settings = obs_source_get_settings(source);
+				Update(settings);
+				obs_data_release(settings);
+				break;
+			}
+
+		case Action::Shutdown:
+			device.ShutdownGraph();
+			return;
+
+		case Action::ConfigVideo:
+			device.OpenDialog(nullptr, DialogType::ConfigVideo);
+			break;
+
+		case Action::ConfigAudio:
+			device.OpenDialog(nullptr, DialogType::ConfigAudio);
+			break;
+
+		case Action::ConfigCrossbar1:
+			device.OpenDialog(nullptr, DialogType::ConfigCrossbar);
+			break;
+
+		case Action::ConfigCrossbar2:
+			device.OpenDialog(nullptr, DialogType::ConfigCrossbar2);
+			break;
+
+		case Action::None:;
+		}
+	}
+}
+
 #define FPS_HIGHEST   0LL
 #define FPS_MATCHING -1LL
 
@@ -640,11 +793,6 @@ bool DShowInput::UpdateAudioConfig(obs_data_t settings)
 
 void DShowInput::Update(obs_data_t settings)
 {
-	if (!comInitialized) {
-		CoInitialize(nullptr);
-		comInitialized = true;
-	}
-
 	if (!device.ResetGraph())
 		return;
 
@@ -685,10 +833,14 @@ static const char *GetDShowInputName(void)
 
 static void *CreateDShowInput(obs_data_t settings, obs_source_t source)
 {
-	DShowInput *dshow = new DShowInput(source);
+	DShowInput *dshow = nullptr;
 
-	/* causes a deferred update in the video thread */
-	obs_source_update(source, nullptr);
+	try {
+		dshow = new DShowInput(source);
+	} catch (const char *error) {
+		blog(LOG_ERROR, "Could not create device '%s': %s",
+				obs_source_get_name(source), error);
+	}
 
 	UNUSED_PARAMETER(settings);
 	return dshow;
@@ -701,7 +853,8 @@ static void DestroyDShowInput(void *data)
 
 static void UpdateDShowInput(void *data, obs_data_t settings)
 {
-	reinterpret_cast<DShowInput*>(data)->Update(settings);
+	UNUSED_PARAMETER(settings);
+	reinterpret_cast<DShowInput*>(data)->QueueAction(Action::Update);
 }
 
 static void GetDShowDefaults(obs_data_t settings)
@@ -959,7 +1112,7 @@ static bool VideoConfigClicked(obs_properties_t props, obs_property_t p,
 		void *data)
 {
 	DShowInput *input = reinterpret_cast<DShowInput*>(data);
-	input->device.OpenDialog(nullptr, DialogType::ConfigVideo);
+	input->QueueAction(Action::ConfigVideo);
 
 	UNUSED_PARAMETER(props);
 	UNUSED_PARAMETER(p);
@@ -970,7 +1123,7 @@ static bool VideoConfigClicked(obs_properties_t props, obs_property_t p,
 		void *data)
 {
 	DShowInput *input = reinterpret_cast<DShowInput*>(data);
-	input->device.OpenDialog(nullptr, DialogType::ConfigAudio);
+	input->QueueAction(Action::ConfigAudio);
 
 	UNUSED_PARAMETER(props);
 	UNUSED_PARAMETER(p);
@@ -981,7 +1134,7 @@ static bool CrossbarConfigClicked(obs_properties_t props, obs_property_t p,
 		void *data)
 {
 	DShowInput *input = reinterpret_cast<DShowInput*>(data);
-	input->device.OpenDialog(nullptr, DialogType::ConfigCrossbar);
+	input->QueueAction(Action::ConfigCrossbar1);
 
 	UNUSED_PARAMETER(props);
 	UNUSED_PARAMETER(p);
@@ -992,7 +1145,7 @@ static bool CrossbarConfigClicked(obs_properties_t props, obs_property_t p,
 		void *data)
 {
 	DShowInput *input = reinterpret_cast<DShowInput*>(data);
-	input->device.OpenDialog(nullptr, DialogType::ConfigCrossbar2);
+	input->QueueAction(Action::ConfigCrossbar2);
 
 	UNUSED_PARAMETER(props);
 	UNUSED_PARAMETER(p);