1
0
Эх сурвалжийг харах

decklink: Add Blackmagic DeckLink capture plugin

This is a cross-platform Blackmagic device capture plugin that makes use
of the manufacturer provided DeckLink SDK.
Skyler Lipthay 10 жил өмнө
parent
commit
4225fa8eee

+ 4 - 0
plugins/decklink/data/locale/en-US.ini

@@ -0,0 +1,4 @@
+BlackmagicDevice="Blackmagic Device"
+Device="Device"
+Mode="Mode"
+Buffering="Use Buffering"

+ 131 - 0
plugins/decklink/decklink-device-discovery.cpp

@@ -0,0 +1,131 @@
+#include "decklink-device-discovery.hpp"
+#include "decklink-device.hpp"
+
+#include <util/threading.h>
+
+DeckLinkDeviceDiscovery::DeckLinkDeviceDiscovery()
+{
+	discovery = CreateDeckLinkDiscoveryInstance();
+	if (discovery == nullptr)
+		blog(LOG_ERROR, "Failed to create IDeckLinkDiscovery");
+}
+
+DeckLinkDeviceDiscovery::~DeckLinkDeviceDiscovery(void)
+{
+	if (discovery != nullptr) {
+		if (initialized)
+			discovery->UninstallDeviceNotifications();
+		for (DeckLinkDevice *device : devices)
+			device->Release();
+	}
+}
+
+bool DeckLinkDeviceDiscovery::Init(void)
+{
+	HRESULT result = E_FAIL;
+
+	if (initialized)
+		return false;
+
+	if (discovery != nullptr)
+		result = discovery->InstallDeviceNotifications(this);
+
+	initialized = result == S_OK;
+	if (initialized)
+		blog(LOG_INFO, "Failed to start search for DeckLink devices");
+
+	return initialized;
+}
+
+DeckLinkDevice *DeckLinkDeviceDiscovery::FindByHash(const char *hash)
+{
+	DeckLinkDevice *ret = nullptr;
+
+	deviceMutex.lock();
+	for (DeckLinkDevice *device : devices) {
+		if (device->GetHash().compare(hash) == 0) {
+			ret = device;
+			ret->AddRef();
+			break;
+		}
+	}
+	deviceMutex.unlock();
+
+	return ret;
+}
+
+HRESULT STDMETHODCALLTYPE DeckLinkDeviceDiscovery::DeckLinkDeviceArrived(
+		IDeckLink *device)
+{
+	DeckLinkDevice *newDev = new DeckLinkDevice(device);
+	if (!newDev->Init()) {
+		delete newDev;
+		return S_OK;
+	}
+
+	std::lock_guard<std::recursive_mutex> lock(deviceMutex);
+
+	devices.push_back(newDev);
+
+	for (DeviceChangeInfo &cb : callbacks)
+		cb.callback(cb.param, newDev, true);
+
+	return S_OK;
+}
+
+HRESULT STDMETHODCALLTYPE DeckLinkDeviceDiscovery::DeckLinkDeviceRemoved(
+		IDeckLink *device)
+{
+	std::lock_guard<std::recursive_mutex> lock(deviceMutex);
+
+	for (size_t i = 0; i < devices.size(); i++) {
+		if (devices[i]->IsDevice(device)) {
+
+			for (DeviceChangeInfo &cb : callbacks)
+				cb.callback(cb.param, devices[i], false);
+
+			devices[i]->Release();
+			devices.erase(devices.begin() + i);
+			break;
+		}
+	}
+	return S_OK;
+}
+
+ULONG STDMETHODCALLTYPE DeckLinkDeviceDiscovery::AddRef(void)
+{
+	return os_atomic_inc_long(&refCount);
+}
+
+HRESULT STDMETHODCALLTYPE DeckLinkDeviceDiscovery::QueryInterface(REFIID iid,
+		LPVOID *ppv)
+{
+	HRESULT result = E_NOINTERFACE;
+
+	*ppv = nullptr;
+
+	CFUUIDBytes unknown = CFUUIDGetUUIDBytes(IUnknownUUID);
+	if (memcmp(&iid, &unknown, sizeof(REFIID)) == 0) {
+		*ppv = this;
+		AddRef();
+		result = S_OK;
+	} else if (memcmp(&iid, &IID_IDeckLinkDeviceNotificationCallback,
+				sizeof(REFIID)) == 0) {
+		*ppv = (IDeckLinkDeviceNotificationCallback *)this;
+		AddRef();
+		result = S_OK;
+	}
+
+	return result;
+}
+
+ULONG STDMETHODCALLTYPE DeckLinkDeviceDiscovery::Release(void)
+{
+	const long newRefCount = os_atomic_dec_long(&refCount);
+	if (newRefCount == 0) {
+		delete this;
+		return 0;
+	}
+
+	return newRefCount;
+}

+ 81 - 0
plugins/decklink/decklink-device-discovery.hpp

@@ -0,0 +1,81 @@
+#pragma once
+
+#include <vector>
+#include <mutex>
+
+#include "decklink.hpp"
+
+class DeckLinkDevice;
+
+typedef void (*DeviceChangeCallback)(void *param, DeckLinkDevice *device,
+		bool added);
+
+struct DeviceChangeInfo {
+	DeviceChangeCallback callback;
+	void *param;
+};
+
+class DeckLinkDeviceDiscovery : public IDeckLinkDeviceNotificationCallback {
+protected:
+	ComPtr<IDeckLinkDiscovery> discovery;
+	long                       refCount = 1;
+	bool                       initialized = false;
+
+	std::recursive_mutex deviceMutex;
+	std::vector<DeckLinkDevice*> devices;
+	std::vector<DeviceChangeInfo> callbacks;
+
+public:
+	DeckLinkDeviceDiscovery();
+	virtual ~DeckLinkDeviceDiscovery(void);
+
+	bool Init();
+
+	HRESULT STDMETHODCALLTYPE DeckLinkDeviceArrived(IDeckLink *device);
+	HRESULT STDMETHODCALLTYPE DeckLinkDeviceRemoved(IDeckLink *device);
+
+	inline void AddCallback(DeviceChangeCallback callback, void *param)
+	{
+		std::lock_guard<std::recursive_mutex> lock(deviceMutex);
+		DeviceChangeInfo info;
+
+		info.callback = callback;
+		info.param = param;
+
+		for (DeviceChangeInfo &curCB : callbacks) {
+			if (curCB.callback == callback &&
+			    curCB.param    == param)
+				return;
+		}
+
+		callbacks.push_back(info);
+	}
+
+	inline void RemoveCallback(DeviceChangeCallback callback, void *param)
+	{
+		std::lock_guard<std::recursive_mutex> lock(deviceMutex);
+
+		for (size_t i = 0; i < callbacks.size(); i++) {
+			DeviceChangeInfo &curCB = callbacks[i];
+
+			if (curCB.callback == callback &&
+			    curCB.param    == param) {
+				callbacks.erase(callbacks.begin() + i);
+				return;
+			}
+		}
+	}
+
+	DeckLinkDevice *FindByHash(const char *hash);
+
+	inline void Lock() {deviceMutex.lock();}
+	inline void Unlock() {deviceMutex.unlock();}
+	inline const std::vector<DeckLinkDevice*> &GetDevices() const
+	{
+		return devices;
+	}
+
+	ULONG STDMETHODCALLTYPE AddRef(void);
+	HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv);
+	ULONG STDMETHODCALLTYPE Release(void);
+};

+ 192 - 0
plugins/decklink/decklink-device-instance.cpp

@@ -0,0 +1,192 @@
+#include "decklink-device-instance.hpp"
+
+#include <util/platform.h>
+#include <util/threading.h>
+
+#include <sstream>
+
+#define LOG(level, message, ...) blog(level, "%s: " message, \
+		obs_source_get_name(this->decklink->GetSource()), ##__VA_ARGS__)
+
+DeckLinkDeviceInstance::DeckLinkDeviceInstance(DeckLink *decklink_,
+		DeckLinkDevice *device_) :
+	currentFrame(), currentPacket(), decklink(decklink_), device(device_)
+{
+	currentFrame.format = VIDEO_FORMAT_UYVY;
+
+	currentPacket.samples_per_sec = 48000;
+	currentPacket.speakers        = SPEAKERS_STEREO;
+	currentPacket.format          = AUDIO_FORMAT_16BIT;
+}
+
+void DeckLinkDeviceInstance::HandleAudioPacket(
+		IDeckLinkAudioInputPacket *audioPacket,
+		const uint64_t timestamp)
+{
+	if (audioPacket == nullptr)
+		return;
+
+	void *bytes;
+	if (audioPacket->GetBytes(&bytes) != S_OK) {
+		LOG(LOG_WARNING, "Failed to get audio packet data");
+		return;
+	}
+
+	currentPacket.data[0]   = (uint8_t *)bytes;
+	currentPacket.frames    = (uint32_t)audioPacket->GetSampleFrameCount();
+	currentPacket.timestamp = timestamp;
+
+	obs_source_output_audio(decklink->GetSource(), &currentPacket);
+}
+
+void DeckLinkDeviceInstance::HandleVideoFrame(
+		IDeckLinkVideoInputFrame *videoFrame, const uint64_t timestamp)
+{
+	if (videoFrame == nullptr)
+		return;
+
+	void *bytes;
+	if (videoFrame->GetBytes(&bytes) != S_OK) {
+		LOG(LOG_WARNING, "Failed to get video frame data");
+		return;
+	}
+
+	currentFrame.data[0]     = (uint8_t *)bytes;
+	currentFrame.linesize[0] = (uint32_t)videoFrame->GetRowBytes();
+	currentFrame.width       = (uint32_t)videoFrame->GetWidth();
+	currentFrame.height      = (uint32_t)videoFrame->GetHeight();
+	currentFrame.timestamp   = timestamp;
+
+	video_format_get_parameters(VIDEO_CS_601, VIDEO_RANGE_PARTIAL,
+			currentFrame.color_matrix, currentFrame.color_range_min,
+			currentFrame.color_range_max);
+
+	obs_source_output_video(decklink->GetSource(), &currentFrame);
+}
+
+bool DeckLinkDeviceInstance::StartCapture(DeckLinkDeviceMode *mode_)
+{
+	if (mode != nullptr)
+		return false;
+	if (mode_ == nullptr)
+		return false;
+
+	LOG(LOG_INFO, "Starting capture...");
+
+	if (!device->GetInput(&input))
+		return false;
+
+	input->SetCallback(this);
+
+	const BMDDisplayMode displayMode = mode_->GetDisplayMode();
+	const HRESULT videoResult = input->EnableVideoInput(displayMode,
+			bmdFormat8BitYUV, bmdVideoInputFlagDefault);
+
+	if (videoResult != S_OK) {
+		LOG(LOG_ERROR, "Failed to enable video input");
+		input->SetCallback(nullptr);
+		return false;
+	}
+
+	const HRESULT audioResult = input->EnableAudioInput(
+			bmdAudioSampleRate48kHz, bmdAudioSampleType16bitInteger,
+			2);
+
+	if (audioResult != S_OK)
+		LOG(LOG_WARNING, "Failed to enable audio input; continuing...");
+
+	if (input->StartStreams() != S_OK) {
+		LOG(LOG_ERROR, "Failed to start streams");
+		input->SetCallback(nullptr);
+		input->DisableVideoInput();
+		input->DisableAudioInput();
+		return false;
+	}
+
+	mode = mode_;
+
+	return true;
+}
+
+bool DeckLinkDeviceInstance::StopCapture(void)
+{
+	if (mode == nullptr || input == nullptr)
+		return false;
+
+	LOG(LOG_INFO, "Stopping capture of '%s'...",
+			GetDevice()->GetDisplayName().c_str());
+
+	input->StopStreams();
+	input->SetCallback(nullptr);
+	input->DisableVideoInput();
+	input->DisableAudioInput();
+
+	mode = nullptr;
+
+	return true;
+}
+
+HRESULT STDMETHODCALLTYPE DeckLinkDeviceInstance::VideoInputFrameArrived(
+		IDeckLinkVideoInputFrame *videoFrame,
+		IDeckLinkAudioInputPacket *audioPacket)
+{
+	const uint64_t timestamp = os_gettime_ns();
+
+	HandleVideoFrame(videoFrame, timestamp);
+	HandleAudioPacket(audioPacket, timestamp);
+
+	return S_OK;
+}
+
+HRESULT STDMETHODCALLTYPE DeckLinkDeviceInstance::VideoInputFormatChanged(
+		BMDVideoInputFormatChangedEvents events,
+		IDeckLinkDisplayMode *newMode,
+		BMDDetectedVideoInputFormatFlags detectedSignalFlags)
+{
+	UNUSED_PARAMETER(events);
+	UNUSED_PARAMETER(newMode);
+	UNUSED_PARAMETER(detectedSignalFlags);
+
+	// There is no implementation for automatic format detection, so this
+	// method goes unused.
+
+	return S_OK;
+}
+
+ULONG STDMETHODCALLTYPE DeckLinkDeviceInstance::AddRef(void)
+{
+	return os_atomic_inc_long(&refCount);
+}
+
+HRESULT STDMETHODCALLTYPE DeckLinkDeviceInstance::QueryInterface(REFIID iid,
+		LPVOID *ppv)
+{
+	HRESULT result = E_NOINTERFACE;
+
+	*ppv = nullptr;
+
+	CFUUIDBytes unknown = CFUUIDGetUUIDBytes(IUnknownUUID);
+	if (memcmp(&iid, &unknown, sizeof(REFIID)) == 0) {
+		*ppv = this;
+		AddRef();
+		result = S_OK;
+	} else if (memcmp(&iid, &IID_IDeckLinkNotificationCallback,
+				sizeof(REFIID)) == 0) {
+		*ppv = (IDeckLinkNotificationCallback *)this;
+		AddRef();
+		result = S_OK;
+	}
+
+	return result;
+}
+
+ULONG STDMETHODCALLTYPE DeckLinkDeviceInstance::Release(void)
+{
+	const long newRefCount = os_atomic_dec_long(&refCount);
+	if (newRefCount == 0) {
+		delete this;
+		return 0;
+	}
+
+	return newRefCount;
+}

+ 45 - 0
plugins/decklink/decklink-device-instance.hpp

@@ -0,0 +1,45 @@
+#pragma once
+
+#include "decklink-device.hpp"
+
+class DeckLinkDeviceInstance : public IDeckLinkInputCallback {
+protected:
+	struct obs_source_frame currentFrame;
+	struct obs_source_audio currentPacket;
+	DeckLink                *decklink = nullptr;
+	DeckLinkDevice          *device = nullptr;
+	DeckLinkDeviceMode      *mode = nullptr;
+	ComPtr<IDeckLinkInput>  input;
+	volatile long           refCount = 1;
+
+	void HandleAudioPacket(IDeckLinkAudioInputPacket *audioPacket,
+			const uint64_t timestamp);
+	void HandleVideoFrame(IDeckLinkVideoInputFrame *videoFrame,
+			const uint64_t timestamp);
+
+public:
+	DeckLinkDeviceInstance(DeckLink *decklink, DeckLinkDevice *device);
+
+	inline DeckLinkDevice *GetDevice() const {return device;}
+	inline long long GetActiveModeId() const
+	{
+		return mode ? mode->GetId() : 0;
+	}
+
+	inline DeckLinkDeviceMode *GetMode() const {return mode;}
+
+	bool StartCapture(DeckLinkDeviceMode *mode);
+	bool StopCapture(void);
+
+	HRESULT STDMETHODCALLTYPE VideoInputFrameArrived(
+			IDeckLinkVideoInputFrame *videoFrame,
+			IDeckLinkAudioInputPacket *audioPacket);
+	HRESULT STDMETHODCALLTYPE VideoInputFormatChanged(
+			BMDVideoInputFormatChangedEvents events,
+			IDeckLinkDisplayMode *newMode,
+			BMDDetectedVideoInputFormatFlags detectedSignalFlags);
+
+	ULONG STDMETHODCALLTYPE AddRef(void);
+	HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv);
+	ULONG STDMETHODCALLTYPE Release(void);
+};

+ 43 - 0
plugins/decklink/decklink-device-mode.cpp

@@ -0,0 +1,43 @@
+#include "decklink-device-mode.hpp"
+
+DeckLinkDeviceMode::DeckLinkDeviceMode(IDeckLinkDisplayMode *mode,
+		long long id) : id(id), mode(mode)
+{
+	if (mode == nullptr)
+		return;
+
+	mode->AddRef();
+
+	decklink_string_t decklinkStringName;
+	if (mode->GetName(&decklinkStringName) == S_OK)
+		DeckLinkStringToStdString(decklinkStringName, name);
+}
+
+DeckLinkDeviceMode::DeckLinkDeviceMode(const std::string& name, long long id) :
+	id(id), mode(nullptr), name(name)
+{
+}
+
+DeckLinkDeviceMode::~DeckLinkDeviceMode(void)
+{
+	if (mode != nullptr)
+		mode->Release();
+}
+
+BMDDisplayMode DeckLinkDeviceMode::GetDisplayMode(void) const
+{
+	if (mode != nullptr)
+		return mode->GetDisplayMode();
+
+	return bmdModeUnknown;
+}
+
+long long DeckLinkDeviceMode::GetId(void) const
+{
+	return id;
+}
+
+const std::string& DeckLinkDeviceMode::GetName(void) const
+{
+	return name;
+}

+ 21 - 0
plugins/decklink/decklink-device-mode.hpp

@@ -0,0 +1,21 @@
+#pragma once
+
+#include "platform.hpp"
+
+#include <string>
+
+class DeckLinkDeviceMode {
+protected:
+	long long            id;
+	IDeckLinkDisplayMode *mode;
+	std::string          name;
+
+public:
+	DeckLinkDeviceMode(IDeckLinkDisplayMode *mode, long long id);
+	DeckLinkDeviceMode(const std::string& name, long long id);
+	virtual ~DeckLinkDeviceMode(void);
+
+	BMDDisplayMode GetDisplayMode(void) const;
+	long long GetId(void) const;
+	const std::string& GetName(void) const;
+};

+ 115 - 0
plugins/decklink/decklink-device.cpp

@@ -0,0 +1,115 @@
+#include <sstream>
+
+#include "decklink-device.hpp"
+
+#include <util/threading.h>
+
+DeckLinkDevice::DeckLinkDevice(IDeckLink *device_) : device(device_)
+{
+}
+
+DeckLinkDevice::~DeckLinkDevice(void)
+{
+	for (DeckLinkDeviceMode *mode : modes)
+		delete mode;
+}
+
+ULONG DeckLinkDevice::AddRef()
+{
+	return os_atomic_inc_long(&refCount);
+}
+
+ULONG DeckLinkDevice::Release()
+{
+	long ret = os_atomic_dec_long(&refCount);
+	if (ret == 0)
+		delete this;
+	return ret;
+}
+
+bool DeckLinkDevice::Init()
+{
+	ComPtr<IDeckLinkInput> input;
+	if (device->QueryInterface(IID_IDeckLinkInput, (void**)&input) != S_OK)
+		return false;
+
+	IDeckLinkDisplayModeIterator *modeIterator;
+	if (input->GetDisplayModeIterator(&modeIterator) == S_OK) {
+		IDeckLinkDisplayMode *displayMode;
+		long long modeId = 1;
+
+		while (modeIterator->Next(&displayMode) == S_OK) {
+			if (displayMode == nullptr)
+				continue;
+
+			DeckLinkDeviceMode *mode =
+				new DeckLinkDeviceMode(displayMode, modeId);
+			modes.push_back(mode);
+			modeIdMap[modeId] = mode;
+			displayMode->Release();
+			++modeId;
+		}
+
+		modeIterator->Release();
+	}
+
+	decklink_string_t decklinkModelName;
+	decklink_string_t decklinkDisplayName;
+
+	if (device->GetModelName(&decklinkModelName) != S_OK)
+		return false;
+	DeckLinkStringToStdString(decklinkModelName, name);
+
+	if (device->GetDisplayName(&decklinkDisplayName) != S_OK)
+		return false;
+	DeckLinkStringToStdString(decklinkDisplayName, displayName);
+
+	hash = displayName;
+
+	ComPtr<IDeckLinkAttributes> attributes;
+	const HRESULT result = device->QueryInterface(IID_IDeckLinkAttributes,
+			(void **)&attributes);
+	if (result != S_OK)
+		return true;
+
+	int64_t value;
+	if (attributes->GetInt(BMDDeckLinkPersistentID, &value) != S_OK)
+		return true;
+
+	std::ostringstream os;
+	os << value << "_" << name;
+	hash = os.str();
+	return true;
+}
+
+bool DeckLinkDevice::GetInput(IDeckLinkInput **input)
+{
+	if (device->QueryInterface(IID_IDeckLinkInput, (void**)input) != S_OK)
+		return false;
+	return true;
+}
+
+DeckLinkDeviceMode *DeckLinkDevice::FindMode(long long id)
+{
+	return modeIdMap[id];
+}
+
+const std::string& DeckLinkDevice::GetDisplayName(void)
+{
+	return displayName;
+}
+
+const std::string& DeckLinkDevice::GetHash(void) const
+{
+	return hash;
+}
+
+const std::vector<DeckLinkDeviceMode *>& DeckLinkDevice::GetModes(void) const
+{
+	return modes;
+}
+
+const std::string& DeckLinkDevice::GetName(void) const
+{
+	return name;
+}

+ 40 - 0
plugins/decklink/decklink-device.hpp

@@ -0,0 +1,40 @@
+#pragma once
+
+#include "decklink.hpp"
+#include "decklink-device-mode.hpp"
+
+#include <map>
+#include <string>
+#include <vector>
+
+class DeckLinkDevice {
+	ComPtr<IDeckLink>                         device;
+	std::map<long long, DeckLinkDeviceMode *> modeIdMap;
+	std::vector<DeckLinkDeviceMode *>         modes;
+	std::string                               name;
+	std::string                               displayName;
+	std::string                               hash;
+	volatile long                             refCount = 1;
+
+public:
+	DeckLinkDevice(IDeckLink *device);
+	~DeckLinkDevice(void);
+
+	ULONG AddRef(void);
+	ULONG Release(void);
+
+	bool Init();
+
+	DeckLinkDeviceMode *FindMode(long long id);
+	const std::string& GetDisplayName(void);
+	const std::string& GetHash(void) const;
+	const std::vector<DeckLinkDeviceMode *>& GetModes(void) const;
+	const std::string& GetName(void) const;
+
+	bool GetInput(IDeckLinkInput **input);
+
+	inline bool IsDevice(IDeckLink *device_)
+	{
+		return device_ == device;
+	}
+};

+ 125 - 0
plugins/decklink/decklink.cpp

@@ -0,0 +1,125 @@
+#include "decklink.hpp"
+#include "decklink-device-discovery.hpp"
+#include "decklink-device-instance.hpp"
+#include "decklink-device-mode.hpp"
+
+#include <util/threading.h>
+
+DeckLink::DeckLink(obs_source_t *source, DeckLinkDeviceDiscovery *discovery_) :
+	discovery(discovery_), source(source)
+{
+	discovery->AddCallback(DeckLink::DevicesChanged, this);
+}
+
+DeckLink::~DeckLink(void)
+{
+	discovery->RemoveCallback(DeckLink::DevicesChanged, this);
+	Deactivate();
+}
+
+DeckLinkDevice *DeckLink::GetDevice() const
+{
+	return instance ? instance->GetDevice() : nullptr;
+}
+
+void DeckLink::DevicesChanged(void *param, DeckLinkDevice *device, bool added)
+{
+	DeckLink *decklink = reinterpret_cast<DeckLink*>(param);
+	std::lock_guard<std::recursive_mutex> lock(decklink->deviceMutex);
+
+	obs_source_update_properties(decklink->source);
+
+	if (added && !decklink->instance) {
+		const char *hash;
+		long long mode;
+		obs_data_t *settings;
+
+		settings = obs_source_get_settings(decklink->source);
+		hash = obs_data_get_string(settings, "device_hash");
+		mode = obs_data_get_int(settings, "mode_id");
+		obs_data_release(settings);
+
+		if (device->GetHash().compare(hash) == 0) {
+			if (!decklink->activateRefs)
+				return;
+			if (decklink->Activate(device, mode))
+				os_atomic_dec_long(&decklink->activateRefs);
+		}
+
+	} else if (!added && decklink->instance) {
+		if (decklink->instance->GetDevice() == device) {
+			os_atomic_inc_long(&decklink->activateRefs);
+			decklink->Deactivate();
+		}
+	}
+}
+
+bool DeckLink::Activate(DeckLinkDevice *device, long long modeId)
+{
+	std::lock_guard<std::recursive_mutex> lock(deviceMutex);
+	DeckLinkDevice *curDevice = GetDevice();
+	const bool same = device == curDevice;
+	const bool isActive = instance != nullptr;
+
+	if (same && (!isActive || instance->GetActiveModeId() == modeId))
+		return false;
+
+	if (isActive)
+		instance->StopCapture();
+
+	if (!same)
+		instance.Set(new DeckLinkDeviceInstance(this, device));
+
+	if (instance == nullptr)
+		return false;
+
+	DeckLinkDeviceMode *mode = GetDevice()->FindMode(modeId);
+	if (mode == nullptr) {
+		instance = nullptr;
+		return false;
+	}
+
+	if (!instance->StartCapture(mode)) {
+		instance = nullptr;
+		return false;
+	}
+
+	os_atomic_inc_long(&activateRefs);
+	SaveSettings();
+	return true;
+}
+
+void DeckLink::Deactivate(void)
+{
+	std::lock_guard<std::recursive_mutex> lock(deviceMutex);
+	if (instance)
+		instance->StopCapture();
+	instance = nullptr;
+
+	os_atomic_dec_long(&activateRefs);
+}
+
+void DeckLink::SaveSettings()
+{
+	if (!instance)
+		return;
+
+	DeckLinkDevice *device = instance->GetDevice();
+	DeckLinkDeviceMode *mode = instance->GetMode();
+
+	obs_data_t *settings = obs_source_get_settings(source);
+
+	obs_data_set_string(settings, "device_hash",
+			device->GetHash().c_str());
+	obs_data_set_string(settings, "device_name",
+			device->GetDisplayName().c_str());
+	obs_data_set_int(settings, "mode_id", instance->GetActiveModeId());
+	obs_data_set_string(settings, "mode_name", mode->GetName().c_str());
+
+	obs_data_release(settings);
+}
+
+obs_source_t *DeckLink::GetSource(void) const
+{
+	return source;
+}

+ 40 - 0
plugins/decklink/decklink.hpp

@@ -0,0 +1,40 @@
+#pragma once
+
+#include "platform.hpp"
+
+#include <obs-module.h>
+
+#include <map>
+#include <vector>
+#include <mutex>
+
+class DeckLinkDeviceDiscovery;
+class DeckLinkDeviceInstance;
+class DeckLinkDevice;
+class DeckLinkDeviceMode;
+
+class DeckLink {
+protected:
+	ComPtr<DeckLinkDeviceInstance>        instance;
+	DeckLinkDeviceDiscovery               *discovery;
+	bool                                  isCapturing = false;
+	obs_source_t                          *source;
+	volatile long                         activateRefs = 0;
+	std::recursive_mutex                  deviceMutex;
+
+	void SaveSettings();
+	static void DevicesChanged(void *param, DeckLinkDevice *device,
+			bool added);
+
+public:
+	DeckLink(obs_source_t *source, DeckLinkDeviceDiscovery *discovery);
+	virtual ~DeckLink(void);
+
+	DeckLinkDevice *GetDevice() const;
+
+	long long GetActiveModeId(void) const;
+	obs_source_t *GetSource(void) const;
+
+	bool Activate(DeckLinkDevice *device, long long modeId);
+	void Deactivate();
+};

+ 16 - 0
plugins/decklink/platform.hpp

@@ -0,0 +1,16 @@
+#pragma once
+
+#if defined(_WIN32)
+// TODO: Windows support
+#elif defined(__APPLE__)
+// TODO: Mac support
+#elif defined(__linux__)
+// TODO: Linux support
+#endif
+
+#include <util/windows/HRError.hpp>
+#include <util/windows/ComPtr.hpp>
+
+#include <string>
+
+bool DeckLinkStringToStdString(decklink_string_t input, std::string& output);

+ 174 - 0
plugins/decklink/plugin-main.cpp

@@ -0,0 +1,174 @@
+#include "decklink.hpp"
+#include "decklink-device.hpp"
+#include "decklink-device-discovery.hpp"
+
+#include <obs-module.h>
+
+OBS_DECLARE_MODULE()
+OBS_MODULE_USE_DEFAULT_LOCALE("decklink", "en-US")
+
+static DeckLinkDeviceDiscovery *deviceEnum = nullptr;
+
+static void decklink_enable_buffering(DeckLink *decklink, bool enabled)
+{
+	obs_source_t *source = decklink->GetSource();
+	uint32_t flags = obs_source_get_flags(source);
+	if (enabled)
+		flags &= ~OBS_SOURCE_FLAG_UNBUFFERED;
+	else
+		flags |= OBS_SOURCE_FLAG_UNBUFFERED;
+	obs_source_set_flags(source, flags);
+}
+
+static void *decklink_create(obs_data_t *settings, obs_source_t *source)
+{
+	DeckLink *decklink = new DeckLink(source, deviceEnum);
+
+	decklink_enable_buffering(decklink,
+			obs_data_get_bool(settings, "buffering"));
+
+	obs_source_update(source, settings);
+	return decklink;
+}
+
+static void decklink_destroy(void *data)
+{
+	DeckLink *decklink = (DeckLink *)data;
+	delete decklink;
+}
+
+static void decklink_update(void *data, obs_data_t *settings)
+{
+	DeckLink *decklink = (DeckLink *)data;
+	const char *hash = obs_data_get_string(settings, "device_hash");
+	long long id = obs_data_get_int(settings, "mode_id");
+
+	decklink_enable_buffering(decklink,
+			obs_data_get_bool(settings, "buffering"));
+
+	ComPtr<DeckLinkDevice> device;
+	device.Set(deviceEnum->FindByHash(hash));
+
+	decklink->Activate(device, id);
+}
+
+static void decklink_get_defaults(obs_data_t *settings)
+{
+	obs_data_set_default_bool(settings, "buffering", true);
+}
+
+static const char *decklink_get_name()
+{
+	return obs_module_text("BlackmagicDevice");
+}
+
+static bool decklink_device_changed(obs_properties_t *props,
+		obs_property_t *list, obs_data_t *settings)
+{
+	const char *name = obs_data_get_string(settings, "device_name");
+	const char *hash = obs_data_get_string(settings, "device_hash");
+	const char *mode = obs_data_get_string(settings, "mode_name");
+	long long modeId = obs_data_get_int(settings, "mode_id");
+
+	size_t itemCount = obs_property_list_item_count(list);
+	bool itemFound = false;
+
+	for (size_t i = 0; i < itemCount; i++) {
+		const char *curHash = obs_property_list_item_string(list, i);
+		if (strcmp(hash, curHash) == 0) {
+			itemFound = true;
+			break;
+		}
+	}
+
+	if (!itemFound) {
+		obs_property_list_insert_string(list, 0, name, hash);
+		obs_property_list_item_disable(list, 0, true);
+	}
+
+	list = obs_properties_get(props, "mode_id");
+
+	obs_property_list_clear(list);
+
+	ComPtr<DeckLinkDevice> device;
+	device.Set(deviceEnum->FindByHash(hash));
+
+	if (!device) {
+		obs_property_list_add_int(list, mode, modeId);
+		obs_property_list_item_disable(list, 0, true);
+	} else {
+		const std::vector<DeckLinkDeviceMode*> &modes =
+			device->GetModes();
+
+		for (DeckLinkDeviceMode *mode : modes) {
+			obs_property_list_add_int(list,
+					mode->GetName().c_str(),
+					mode->GetId());
+		}
+	}
+
+	return true;
+}
+
+static void fill_out_devices(obs_property_t *list)
+{
+	deviceEnum->Lock();
+
+	const std::vector<DeckLinkDevice*> &devices = deviceEnum->GetDevices();
+	for (DeckLinkDevice *device : devices) {
+		obs_property_list_add_string(list,
+				device->GetDisplayName().c_str(),
+				device->GetHash().c_str());
+	}
+
+	deviceEnum->Unlock();
+}
+
+static obs_properties_t *decklink_get_properties(void *data)
+{
+	obs_properties_t *props = obs_properties_create();
+
+	obs_property_t *list = obs_properties_add_list(props, "device_hash",
+			obs_module_text("Device"), OBS_COMBO_TYPE_LIST,
+			OBS_COMBO_FORMAT_STRING);
+	obs_property_set_modified_callback(list, decklink_device_changed);
+
+	fill_out_devices(list);
+
+	list = obs_properties_add_list(props, "mode_id",
+			obs_module_text("Mode"), OBS_COMBO_TYPE_LIST,
+			OBS_COMBO_FORMAT_INT);
+
+	obs_properties_add_bool(props, "buffering",
+			obs_module_text("Buffering"));
+
+	UNUSED_PARAMETER(data);
+	return props;
+}
+
+bool obs_module_load(void)
+{
+	deviceEnum = new DeckLinkDeviceDiscovery();
+	if (!deviceEnum->Init())
+		return true;
+
+	struct obs_source_info info = {};
+	info.id             = "decklink-input";
+	info.type           = OBS_SOURCE_TYPE_INPUT;
+	info.output_flags   = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_AUDIO;
+	info.create         = decklink_create;
+	info.destroy        = decklink_destroy;
+	info.get_defaults   = decklink_get_defaults;
+	info.get_name       = decklink_get_name;
+	info.get_properties = decklink_get_properties;
+	info.update         = decklink_update;
+
+	obs_register_source(&info);
+
+	return true;
+}
+
+void obs_module_unload(void)
+{
+	delete deviceEnum;
+}