Browse Source

aja: Capture and Output plugin for AJA Video Systems IO devices

* aja: Initial commit of AJA capture/output plugin

* aja: Fix clang-format on aja-output-ui code

* aja: Remove script used during dev/testing

* aja: Address pull request feedback from @RytoEX

* aja: Remove the SDK sources and update CMakeLists to point to new headers-only/static libs dependency distribution.

* aja: Only build AJA plugin on x64 on macOS for now

* aja: Remove the non-English placeholder locale files. The english strings/files will be produced via crowdin, according to @ddrboxman.

* aja: Add FindLibAJANTV2.cmake script to locate the ajantv2 headers and static libs in the OBS external deps package(s). Tested on Windows x64. macOS and Linux x64 TBD.

* aja: Add ajantv2/includes to FindLibAJANTV2 include search paths

* aja: Remove commented code from aja CMakeLists

* aja: Remove debug code and comments that are no longer needed.

* aja: Fix indentation

* aja: Remove disablement of clang-format in routing table and SDIWireFormat map

* aja: Use spaces for all indentation in widget crosspoint arrays where we disable clang-format

* aja: Address code style comments made by @RytoEX

* aja: Fix uneven indentation

* aja: More fixes to if/else placement and remove superfluous comments.

* aja: Rename 'dwns' to 'deactivateWhileNotShowing' for clarity. The DeckLink plugin still uses the variable name 'dwns' and should be changed, if desired, in a separate PR.

* aja: Remove X11Extras dependency from AJA Output frontend plugin

* aja: Add patch from Jim to find AJA release/debug libs

* aja: Improve AV sync of queued video/audio sent to the AJA card in the AJA Output plugin.
Paul Hindt 4 years ago
parent
commit
ce3ae8e423
42 changed files with 9746 additions and 0 deletions
  1. 2 0
      CI/full-build-macos.sh
  2. 1 0
      UI/frontend-plugins/CMakeLists.txt
  3. 169 0
      UI/frontend-plugins/aja-output-ui/AJAOutputUI.cpp
  4. 33 0
      UI/frontend-plugins/aja-output-ui/AJAOutputUI.h
  5. 113 0
      UI/frontend-plugins/aja-output-ui/CMakeLists.txt
  6. 318 0
      UI/frontend-plugins/aja-output-ui/aja-ui-main.cpp
  7. 10 0
      UI/frontend-plugins/aja-output-ui/aja-ui-main.h
  8. 0 0
      UI/frontend-plugins/aja-output-ui/data/.keepme
  9. 3 0
      UI/frontend-plugins/aja-output-ui/data/locale/en-US.ini
  10. 113 0
      UI/frontend-plugins/aja-output-ui/forms/output.ui
  11. 113 0
      cmake/Modules/FindLibAJANTV2.cmake
  12. 1 0
      formatcode.sh
  13. 1 0
      plugins/CMakeLists.txt
  14. 102 0
      plugins/aja/CMakeLists.txt
  15. 78 0
      plugins/aja/README.md
  16. 610 0
      plugins/aja/aja-card-manager.cpp
  17. 103 0
      plugins/aja/aja-card-manager.hpp
  18. 892 0
      plugins/aja/aja-common.cpp
  19. 92 0
      plugins/aja/aja-common.hpp
  20. 88 0
      plugins/aja/aja-enums.hpp
  21. 1267 0
      plugins/aja/aja-output.cpp
  22. 160 0
      plugins/aja/aja-output.hpp
  23. 436 0
      plugins/aja/aja-props.cpp
  24. 108 0
      plugins/aja/aja-props.hpp
  25. 823 0
      plugins/aja/aja-routing.cpp
  26. 139 0
      plugins/aja/aja-routing.hpp
  27. 1184 0
      plugins/aja/aja-source.cpp
  28. 83 0
      plugins/aja/aja-source.hpp
  29. 91 0
      plugins/aja/aja-ui-props.hpp
  30. 475 0
      plugins/aja/aja-widget-io.cpp
  31. 42 0
      plugins/aja/aja-widget-io.hpp
  32. 0 0
      plugins/aja/data/.keepme
  33. 15 0
      plugins/aja/data/locale/en-US.ini
  34. 34 0
      plugins/aja/main.cpp
  35. 22 0
      plugins/aja/routing/hdmi_rgb_capture.h
  36. 28 0
      plugins/aja/routing/hdmi_rgb_display.h
  37. 47 0
      plugins/aja/routing/hdmi_ycbcr_capture.h
  38. 64 0
      plugins/aja/routing/hdmi_ycbcr_display.h
  39. 466 0
      plugins/aja/routing/sdi_rgb_capture.h
  40. 468 0
      plugins/aja/routing/sdi_rgb_display.h
  41. 466 0
      plugins/aja/routing/sdi_ycbcr_capture.h
  42. 486 0
      plugins/aja/routing/sdi_ycbcr_display.h

+ 2 - 0
CI/full-build-macos.sh

@@ -329,6 +329,8 @@ bundle_dylibs() {
         ./OBS.app/Contents/PlugIns/obs-x264.so
         ./OBS.app/Contents/PlugIns/text-freetype2.so
         ./OBS.app/Contents/PlugIns/obs-outputs.so
+        ./OBS.app/Contents/PlugIns/aja.so
+        ./OBS.app/Contents/PlugIns/aja-output-ui.so
         )
     if ! [ "${MACOS_CEF_BUILD_VERSION:-${CI_MACOS_CEF_VERSION}}" -le 3770 ]; then
         "${CI_SCRIPTS}/app/dylibbundler" -cd -of -a ./OBS.app -q -f \

+ 1 - 0
UI/frontend-plugins/CMakeLists.txt

@@ -1,3 +1,4 @@
 add_subdirectory(decklink-output-ui)
 add_subdirectory(frontend-tools)
 add_subdirectory(decklink-captions)
+add_subdirectory(aja-output-ui)

+ 169 - 0
UI/frontend-plugins/aja-output-ui/AJAOutputUI.cpp

@@ -0,0 +1,169 @@
+#include "AJAOutputUI.h"
+#include "aja-ui-main.h"
+
+#include "../../../plugins/aja/aja-ui-props.hpp"
+#include "../../../plugins/aja/aja-enums.hpp"
+
+#include <ajantv2/includes/ntv2enums.h>
+
+#include <obs-module.h>
+#include <util/platform.h>
+#include <util/util.hpp>
+
+AJAOutputUI::AJAOutputUI(QWidget *parent) : QDialog(parent), ui(new Ui_Output)
+{
+	ui->setupUi(this);
+
+	setSizeGripEnabled(true);
+
+	setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+
+	propertiesView = nullptr;
+	previewPropertiesView = nullptr;
+}
+
+void AJAOutputUI::ShowHideDialog()
+{
+	SetupPropertiesView();
+	SetupPreviewPropertiesView();
+
+	setVisible(!isVisible());
+}
+
+void AJAOutputUI::SetupPropertiesView()
+{
+	if (propertiesView)
+		delete propertiesView;
+
+	obs_data_t *settings = obs_data_create();
+
+	OBSData data = load_settings(kProgramPropsFilename);
+	if (data) {
+		obs_data_apply(settings, data);
+	} else {
+		// apply default settings
+		obs_data_set_int(settings, kUIPropOutput.id,
+				 static_cast<long long>(IOSelection::Invalid));
+		obs_data_set_int(settings, kUIPropVideoFormatSelect.id,
+				 static_cast<long long>(NTV2_FORMAT_720p_5994));
+		obs_data_set_int(settings, kUIPropPixelFormatSelect.id,
+				 static_cast<long long>(NTV2_FBF_8BIT_YCBCR));
+		obs_data_set_int(settings, kUIPropSDI4KTransport.id,
+				 static_cast<long long>(
+					 SDI4KTransport::TwoSampleInterleave));
+	}
+
+	// Assign an ID to the program output plugin instance for channel usage tracking
+	obs_data_set_string(settings, kUIPropAJAOutputID.id, kProgramOutputID);
+
+	propertiesView = new OBSPropertiesView(
+		settings, "aja_output",
+		(PropertiesReloadCallback)obs_get_output_properties, 170);
+
+	ui->propertiesLayout->addWidget(propertiesView);
+	obs_data_release(settings);
+
+	connect(propertiesView, SIGNAL(Changed()), this,
+		SLOT(PropertiesChanged()));
+}
+
+void AJAOutputUI::SaveSettings(const char *filename, obs_data_t *settings)
+{
+	BPtr<char> modulePath =
+		obs_module_get_config_path(obs_current_module(), "");
+
+	os_mkdirs(modulePath);
+
+	BPtr<char> path =
+		obs_module_get_config_path(obs_current_module(), filename);
+
+	if (settings)
+		obs_data_save_json_safe(settings, path, "tmp", "bak");
+}
+
+void AJAOutputUI::SetupPreviewPropertiesView()
+{
+	if (previewPropertiesView)
+		delete previewPropertiesView;
+
+	obs_data_t *settings = obs_data_create();
+
+	OBSData data = load_settings(kPreviewPropsFilename);
+	if (data) {
+		obs_data_apply(settings, data);
+	} else {
+		// apply default settings
+		obs_data_set_int(settings, kUIPropOutput.id,
+				 static_cast<long long>(IOSelection::Invalid));
+		obs_data_set_int(settings, kUIPropVideoFormatSelect.id,
+				 static_cast<long long>(NTV2_FORMAT_720p_5994));
+		obs_data_set_int(settings, kUIPropPixelFormatSelect.id,
+				 static_cast<long long>(NTV2_FBF_8BIT_YCBCR));
+		obs_data_set_int(settings, kUIPropSDI4KTransport.id,
+				 static_cast<long long>(
+					 SDI4KTransport::TwoSampleInterleave));
+	}
+
+	// Assign an ID to the program output plugin instance for channel usage tracking
+	obs_data_set_string(settings, kUIPropAJAOutputID.id, kPreviewOutputID);
+
+	previewPropertiesView = new OBSPropertiesView(
+		settings, "aja_output",
+		(PropertiesReloadCallback)obs_get_output_properties, 170);
+
+	ui->previewPropertiesLayout->addWidget(previewPropertiesView);
+	obs_data_release(settings);
+
+	connect(previewPropertiesView, SIGNAL(Changed()), this,
+		SLOT(PreviewPropertiesChanged()));
+}
+
+void AJAOutputUI::on_outputButton_clicked()
+{
+	SaveSettings(kProgramPropsFilename, propertiesView->GetSettings());
+	output_toggle();
+}
+
+void AJAOutputUI::PropertiesChanged()
+{
+	SaveSettings(kProgramPropsFilename, propertiesView->GetSettings());
+}
+
+void AJAOutputUI::OutputStateChanged(bool active)
+{
+	QString text;
+	if (active) {
+		text = QString(obs_module_text("Stop"));
+	} else {
+		text = QString(obs_module_text("Start"));
+	}
+
+	ui->outputButton->setChecked(active);
+	ui->outputButton->setText(text);
+}
+
+void AJAOutputUI::on_previewOutputButton_clicked()
+{
+	SaveSettings(kPreviewPropsFilename,
+		     previewPropertiesView->GetSettings());
+	preview_output_toggle();
+}
+
+void AJAOutputUI::PreviewPropertiesChanged()
+{
+	SaveSettings(kPreviewPropsFilename,
+		     previewPropertiesView->GetSettings());
+}
+
+void AJAOutputUI::PreviewOutputStateChanged(bool active)
+{
+	QString text;
+	if (active) {
+		text = QString(obs_module_text("Stop"));
+	} else {
+		text = QString(obs_module_text("Start"));
+	}
+
+	ui->previewOutputButton->setChecked(active);
+	ui->previewOutputButton->setText(text);
+}

+ 33 - 0
UI/frontend-plugins/aja-output-ui/AJAOutputUI.h

@@ -0,0 +1,33 @@
+#pragma once
+
+#include <QDialog>
+
+#include "ui_output.h"
+#include "../../UI/properties-view.hpp"
+
+class AJAOutputUI : public QDialog {
+	Q_OBJECT
+private:
+	OBSPropertiesView *propertiesView;
+	OBSPropertiesView *previewPropertiesView;
+
+public slots:
+	void on_outputButton_clicked();
+	void PropertiesChanged();
+	void OutputStateChanged(bool);
+
+	void on_previewOutputButton_clicked();
+	void PreviewPropertiesChanged();
+	void PreviewOutputStateChanged(bool);
+
+public:
+	std::unique_ptr<Ui_Output> ui;
+	AJAOutputUI(QWidget *parent);
+
+	void ShowHideDialog();
+
+	void SaveSettings(const char *filename, obs_data_t *settings);
+
+	void SetupPropertiesView();
+	void SetupPreviewPropertiesView();
+};

+ 113 - 0
UI/frontend-plugins/aja-output-ui/CMakeLists.txt

@@ -0,0 +1,113 @@
+project(aja-output-ui)
+
+if(DISABLE_AJA)
+	message(STATUS "aja-output-ui plugin disabled")
+	return()
+endif()
+
+if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+	find_package(LibAJANTV2)
+	if (NOT LIBAJANTV2_FOUND)
+		message(STATUS "aja-output-ui plugin disabled (deps not found)")
+		return()
+	else()
+		message("aja-output-ui includes: ${LIBAJANTV2_INCLUDE_DIRS}")
+		message("aja-output-ui libs: ${LIBAJANTV2_LIBRARIES}")
+	endif()
+else()
+	message(STATUS "aja-output-ui disabled (32-bit not supported)")
+	return()
+endif()
+
+if(APPLE)
+	find_library(COCOA Cocoa)
+	include_directories(${COCOA})
+endif()
+
+if(UNIX AND NOT APPLE)
+	find_package(X11 REQUIRED)
+	link_libraries(${X11_LIBRARIES})
+	include_directories(${X11_INCLUDE_DIR})
+
+	find_package(Qt5X11Extras REQUIRED)
+endif()
+
+set(aja-output-ui_HEADERS
+	${aja-output-ui_HEADERS}
+	../../qt-wrappers.hpp
+	../../properties-view.hpp
+	../../properties-view.moc.hpp
+	../../vertical-scroll-area.hpp
+	../../double-slider.hpp
+	../../slider-ignorewheel.hpp
+	../../combobox-ignorewheel.hpp
+	../../spinbox-ignorewheel.hpp
+	AJAOutputUI.h
+	aja-ui-main.h
+	../../../plugins/aja/aja-ui-props.hpp
+	../../../plugins/aja/aja-enums.hpp)
+
+set(aja-output-ui_SOURCES
+	${aja-output-ui_SOURCES}
+	../../qt-wrappers.cpp
+	../../properties-view.cpp
+	../../vertical-scroll-area.cpp
+	../../double-slider.cpp
+	../../slider-ignorewheel.cpp
+	../../combobox-ignorewheel.cpp
+	../../spinbox-ignorewheel.cpp
+	AJAOutputUI.cpp
+	aja-ui-main.cpp)
+
+set(aja-output-ui_UI
+	${aja-output-ui_UI}
+	forms/output.ui)
+
+if(WIN32)
+	set(MODULE_DESCRIPTION "OBS AJA Output UI")
+	configure_file(${CMAKE_SOURCE_DIR}/cmake/winrc/obs-module.rc.in aja-output-ui.rc)
+	list(APPEND aja-output-ui_SOURCES
+		aja-output-ui.rc)
+endif()
+
+if(APPLE)
+	set(aja-output-ui_PLATFORM_LIBS
+		${COCOA})
+endif()
+
+qt5_wrap_ui(aja-output-ui_UI_HEADERS
+	${aja-output-ui_UI})
+
+add_library(aja-output-ui MODULE
+	${aja-output-ui_HEADERS}
+	${aja-output-ui_SOURCES}
+	${aja-output-ui_UI_HEADERS})
+
+if (APPLE)
+	target_compile_definitions(aja-output-ui PUBLIC
+		AJAMac
+		AJA_MAC)
+elseif(WIN32)
+	target_compile_definitions(aja-output-ui PUBLIC
+		AJA_WINDOWS
+		_WINDOWS
+		WIN32
+		MSWindows)
+elseif(UNIX AND NOT APPLE)
+	target_compile_definitions(aja-output-ui PUBLIC
+		AJA_LINUX
+		AJALinux)
+endif()
+
+target_include_directories(aja-output-ui PUBLIC
+	${LIBAJANTV2_INCLUDE_DIRS})
+
+target_link_libraries(aja-output-ui
+	${frontend-tools_PLATFORM_LIBS}
+	obs-frontend-api
+	Qt5::Widgets
+	libobs)
+
+set_target_properties(aja-output-ui PROPERTIES FOLDER "frontend")
+
+install_obs_plugin_with_data(aja-output-ui data)

+ 318 - 0
UI/frontend-plugins/aja-output-ui/aja-ui-main.cpp

@@ -0,0 +1,318 @@
+#include "aja-ui-main.h"
+#include "AJAOutputUI.h"
+
+#include "../../../plugins/aja/aja-ui-props.hpp"
+
+#include <obs-module.h>
+#include <obs-frontend-api.h>
+#include <QMainWindow>
+#include <QAction>
+#include <util/util.hpp>
+#include <util/platform.h>
+#include <media-io/video-io.h>
+#include <media-io/video-frame.h>
+
+OBS_DECLARE_MODULE()
+OBS_MODULE_USE_DEFAULT_LOCALE("aja-output-ui", "en-US")
+
+AJAOutputUI *doUI;
+
+bool main_output_running = false;
+bool preview_output_running = false;
+
+obs_output_t *output;
+
+struct preview_output {
+	bool enabled;
+	obs_source_t *current_source;
+	obs_output_t *output;
+
+	video_t *video_queue;
+	gs_texrender_t *texrender;
+	gs_stagesurf_t *stagesurface;
+	uint8_t *video_data;
+	uint32_t video_linesize;
+
+	obs_video_info ovi;
+};
+
+static struct preview_output context = {0};
+
+OBSData load_settings(const char *filename)
+{
+	BPtr<char> path =
+		obs_module_get_config_path(obs_current_module(), filename);
+	BPtr<char> jsonData = os_quick_read_utf8_file(path);
+	if (!!jsonData) {
+		obs_data_t *data = obs_data_create_from_json(jsonData);
+		OBSData dataRet(data);
+		obs_data_release(data);
+
+		return dataRet;
+	}
+
+	return nullptr;
+}
+
+void output_stop()
+{
+	obs_output_stop(output);
+	obs_output_release(output);
+	main_output_running = false;
+	doUI->OutputStateChanged(false);
+}
+
+void output_start()
+{
+	OBSData settings = load_settings(kProgramPropsFilename);
+
+	if (settings != nullptr) {
+		output = obs_output_create("aja_output", kProgramOutputID,
+					   settings, NULL);
+
+		bool started = obs_output_start(output);
+		obs_data_release(settings);
+
+		main_output_running = started;
+
+		doUI->OutputStateChanged(started);
+
+		if (!started)
+			output_stop();
+	}
+}
+
+void output_toggle()
+{
+	if (main_output_running)
+		output_stop();
+	else
+		output_start();
+}
+
+void on_preview_scene_changed(enum obs_frontend_event event, void *param);
+void render_preview_source(void *param, uint32_t cx, uint32_t cy);
+
+void preview_output_stop()
+{
+	obs_output_stop(context.output);
+	obs_output_release(context.output);
+	video_output_stop(context.video_queue);
+
+	obs_remove_main_render_callback(render_preview_source, &context);
+	obs_frontend_remove_event_callback(on_preview_scene_changed, &context);
+
+	obs_source_release(context.current_source);
+
+	obs_enter_graphics();
+	gs_stagesurface_destroy(context.stagesurface);
+	gs_texrender_destroy(context.texrender);
+	obs_leave_graphics();
+
+	video_output_close(context.video_queue);
+
+	preview_output_running = false;
+	doUI->PreviewOutputStateChanged(false);
+}
+
+void preview_output_start()
+{
+	OBSData settings = load_settings(kPreviewPropsFilename);
+
+	if (settings != nullptr) {
+		context.output = obs_output_create(
+			"aja_output", kPreviewOutputID, settings, NULL);
+
+		obs_get_video_info(&context.ovi);
+
+		uint32_t width = context.ovi.base_width;
+		uint32_t height = context.ovi.base_height;
+
+		obs_enter_graphics();
+		context.texrender = gs_texrender_create(GS_BGRA, GS_ZS_NONE);
+		context.stagesurface =
+			gs_stagesurface_create(width, height, GS_BGRA);
+		obs_leave_graphics();
+
+		const video_output_info *mainVOI =
+			video_output_get_info(obs_get_video());
+
+		video_output_info vi = {0};
+		vi.format = VIDEO_FORMAT_BGRA;
+		vi.width = width;
+		vi.height = height;
+		vi.fps_den = context.ovi.fps_den;
+		vi.fps_num = context.ovi.fps_num;
+		vi.cache_size = 16;
+		vi.colorspace = mainVOI->colorspace;
+		vi.range = mainVOI->range;
+		vi.name = kPreviewOutputID;
+
+		video_output_open(&context.video_queue, &vi);
+
+		obs_frontend_add_event_callback(on_preview_scene_changed,
+						&context);
+		if (obs_frontend_preview_program_mode_active()) {
+			context.current_source =
+				obs_frontend_get_current_preview_scene();
+		} else {
+			context.current_source =
+				obs_frontend_get_current_scene();
+		}
+		obs_add_main_render_callback(render_preview_source, &context);
+
+		obs_output_set_media(context.output, context.video_queue,
+				     obs_get_audio());
+		bool started = obs_output_start(context.output);
+
+		obs_data_release(settings);
+
+		preview_output_running = started;
+		doUI->PreviewOutputStateChanged(started);
+
+		if (!started)
+			preview_output_stop();
+	}
+}
+
+void preview_output_toggle()
+{
+	if (preview_output_running)
+		preview_output_stop();
+	else
+		preview_output_start();
+}
+
+void on_preview_scene_changed(enum obs_frontend_event event, void *param)
+{
+	auto ctx = (struct preview_output *)param;
+	switch (event) {
+	case OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED:
+	case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:
+		obs_source_release(ctx->current_source);
+		ctx->current_source = obs_frontend_get_current_preview_scene();
+		break;
+	case OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED:
+		obs_source_release(ctx->current_source);
+		ctx->current_source = obs_frontend_get_current_scene();
+		break;
+	case OBS_FRONTEND_EVENT_SCENE_CHANGED:
+		if (!obs_frontend_preview_program_mode_active()) {
+			obs_source_release(ctx->current_source);
+			ctx->current_source = obs_frontend_get_current_scene();
+		}
+		break;
+	default:
+		break;
+	}
+}
+
+void render_preview_source(void *param, uint32_t cx, uint32_t cy)
+{
+	UNUSED_PARAMETER(cx);
+	UNUSED_PARAMETER(cy);
+
+	auto ctx = (struct preview_output *)param;
+
+	if (!ctx->current_source)
+		return;
+
+	uint32_t width = obs_source_get_base_width(ctx->current_source);
+	uint32_t height = obs_source_get_base_height(ctx->current_source);
+
+	gs_texrender_reset(ctx->texrender);
+
+	if (gs_texrender_begin(ctx->texrender, width, height)) {
+		struct vec4 background;
+		vec4_zero(&background);
+
+		gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0);
+		gs_ortho(0.0f, (float)width, 0.0f, (float)height, -100.0f,
+			 100.0f);
+
+		gs_blend_state_push();
+		gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
+
+		obs_source_video_render(ctx->current_source);
+
+		gs_blend_state_pop();
+		gs_texrender_end(ctx->texrender);
+
+		struct video_frame output_frame;
+		if (video_output_lock_frame(ctx->video_queue, &output_frame, 1,
+					    os_gettime_ns())) {
+			gs_stage_texture(
+				ctx->stagesurface,
+				gs_texrender_get_texture(ctx->texrender));
+
+			if (gs_stagesurface_map(ctx->stagesurface,
+						&ctx->video_data,
+						&ctx->video_linesize)) {
+				uint32_t linesize = output_frame.linesize[0];
+				for (uint32_t i = 0; i < ctx->ovi.base_height;
+				     i++) {
+					uint32_t dst_offset = linesize * i;
+					uint32_t src_offset =
+						ctx->video_linesize * i;
+					memcpy(output_frame.data[0] +
+						       dst_offset,
+					       ctx->video_data + src_offset,
+					       linesize);
+				}
+
+				gs_stagesurface_unmap(ctx->stagesurface);
+				ctx->video_data = nullptr;
+			}
+
+			video_output_unlock_frame(ctx->video_queue);
+		}
+	}
+}
+
+void addOutputUI(void)
+{
+	QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(
+		"AJA I/O Device Output");
+
+	QMainWindow *window = (QMainWindow *)obs_frontend_get_main_window();
+
+	obs_frontend_push_ui_translation(obs_module_get_string);
+	doUI = new AJAOutputUI(window);
+	obs_frontend_pop_ui_translation();
+
+	auto cb = []() { doUI->ShowHideDialog(); };
+
+	action->connect(action, &QAction::triggered, cb);
+}
+
+static void OBSEvent(enum obs_frontend_event event, void *)
+{
+	if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) {
+		OBSData settings = load_settings(kProgramPropsFilename);
+
+		if (settings &&
+		    obs_data_get_bool(settings, kUIPropAutoStartOutput.id))
+			output_start();
+
+		OBSData previewSettings = load_settings(kPreviewPropsFilename);
+
+		if (previewSettings &&
+		    obs_data_get_bool(previewSettings,
+				      kUIPropAutoStartOutput.id))
+			preview_output_start();
+	} else if (event == OBS_FRONTEND_EVENT_EXIT) {
+		if (main_output_running)
+			output_stop();
+		if (preview_output_running)
+			preview_output_stop();
+	}
+}
+
+bool obs_module_load(void)
+{
+	addOutputUI();
+
+	obs_frontend_add_event_callback(OBSEvent, nullptr);
+
+	return true;
+}

+ 10 - 0
UI/frontend-plugins/aja-output-ui/aja-ui-main.h

@@ -0,0 +1,10 @@
+#pragma once
+
+#include <obs.hpp>
+
+static const char *kProgramPropsFilename = "ajaOutputProps.json";
+static const char *kPreviewPropsFilename = "ajaPreviewOutputProps.json";
+
+OBSData load_settings(const char *filename);
+void output_toggle();
+void preview_output_toggle();

+ 0 - 0
UI/frontend-plugins/aja-output-ui/data/.keepme


+ 3 - 0
UI/frontend-plugins/aja-output-ui/data/locale/en-US.ini

@@ -0,0 +1,3 @@
+AJAOutput.Device="AJA I/O Device Output"
+AJAOutput.ProgramOutput="Program Output"
+AJAOutput.PreviewOutput="Preview Output"

+ 113 - 0
UI/frontend-plugins/aja-output-ui/forms/output.ui

@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Output</class>
+ <widget class="QDialog" name="Output">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>800</width>
+    <height>540</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>AJAOutput.Device</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <property name="modal">
+   <bool>false</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="sizeConstraint">
+    <enum>QLayout::SetDefaultConstraint</enum>
+   </property>
+   <item>
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>AJAOutput.ProgramOutput</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QVBoxLayout" name="propertiesLayout"/>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <property name="spacing">
+      <number>6</number>
+     </property>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="outputButton">
+       <property name="text">
+        <string>Start</string>
+       </property>
+       <property name="checkable">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>AJAOutput.PreviewOutput</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QVBoxLayout" name="previewPropertiesLayout"/>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <item>
+      <spacer name="horizontalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="previewOutputButton">
+       <property name="text">
+        <string>Start</string>
+       </property>
+       <property name="checkable">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 113 - 0
cmake/Modules/FindLibAJANTV2.cmake

@@ -0,0 +1,113 @@
+# Once done these will be defined:
+#
+#  LIBAJANTV2_FOUND
+#  LIBAJANTV2_INCLUDE_DIRS
+#  LIBAJANTV2_LIBRARIES
+#
+find_package(PkgConfig QUIET)
+if (PKG_CONFIG_FOUND)
+	pkg_check_modules(_AJA QUIET ajantv2)
+endif()
+
+if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+	set(_lib_suffix 64)
+else()
+	set(_lib_suffix 32)
+endif()
+
+find_path(AJA_LIBRARIES_INCLUDE_DIR
+	NAMES ajalibraries
+	HINTS
+		ENV AJASDKPath${_lib_suffix}
+		ENV AJASDKPath
+		ENV DepsPath${_lib_suffix}
+		ENV DepsPath
+		${AJASDKPath${_lib_suffix}}
+		${AJASDKPath}
+		${DepsPath${_lib_suffix}}
+		${DepsPath}
+		${_AJA_NTV2_INCLUDE_DIRS}
+	PATHS
+		/usr/include /usr/local/include /opt/local/include /sw/include
+	PATH_SUFFIXES
+		include)
+
+find_library(AJA_NTV2_LIB
+	NAMES ${_AJA_NTV2_LIBRARIES} ajantv2 libajantv2
+	HINTS
+		ENV AJASDKPath${_lib_suffix}
+		ENV AJASDKPath
+		ENV DepsPath${_lib_suffix}
+		ENV DepsPath
+		${AJASDKPath${_lib_suffix}}
+		${AJASDKPath}
+		${DepsPath${_lib_suffix}}
+		${DepsPath}
+		${_AJA_NTV2_LIBRARY_DIRS}
+	PATHS
+		/usr/lib /usr/local/lib /opt/local/lib /sw/lib
+	PATH_SUFFIXES
+		lib${_lib_suffix} lib
+		libs${_lib_suffix} libs
+		bin${_lib_suffix} bin
+		../lib${_lib_suffix} ../lib
+		../libs${_lib_suffix} ../libs
+		../bin${_lib_suffix} ../bin)
+
+find_library(AJA_NTV2_DEBUG_LIB
+	NAMES ajantv2d libajantv2d
+	HINTS
+		ENV AJASDKPath${_lib_suffix}
+		ENV AJASDKPath
+		ENV DepsPath${_lib_suffix}
+		ENV DepsPath
+		${AJASDKPath${_lib_suffix}}
+		${AJASDKPath}
+		${DepsPath${_lib_suffix}}
+		${DepsPath}
+		${_AJA_NTV2_LIBRARY_DIRS}
+	PATHS
+		/usr/lib /usr/local/lib /opt/local/lib /sw/lib
+	PATH_SUFFIXES
+		lib${_lib_suffix} lib
+		libs${_lib_suffix} libs
+		bin${_lib_suffix} bin
+		../lib${_lib_suffix} ../lib
+		../libs${_lib_suffix} ../libs
+		../bin${_lib_suffix} ../bin)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(LibAJANTV2 DEFAULT_MSG AJA_LIBRARIES_INCLUDE_DIR AJA_NTV2_LIB)
+mark_as_advanced(AJA_LIBRARIES_INCLUDE_DIR AJA_NTV2_LIB)
+
+if(LIBAJANTV2_FOUND)
+	set(AJA_LIBRARIES_INCLUDE_DIR
+		${AJA_LIBRARIES_INCLUDE_DIR}/ajalibraries)
+	set(AJA_LIBRARIES_INCLUDE_DIRS
+		${AJA_LIBRARIES_INCLUDE_DIR}
+		${AJA_LIBRARIES_INCLUDE_DIR}/ajaanc
+		${AJA_LIBRARIES_INCLUDE_DIR}/ajabase
+		${AJA_LIBRARIES_INCLUDE_DIR}/ajantv2
+		${AJA_LIBRARIES_INCLUDE_DIR}/ajantv2/includes
+		${AJA_LIBRARIES_INCLUDE_DIR}/ajantv2/src)
+	if (WIN32)
+		set(AJA_LIBRARIES_INCLUDE_DIRS
+			${AJA_LIBRARIES_INCLUDE_DIRS}
+			${AJA_LIBRARIES_INCLUDE_DIR}/ajantv2/src/win)
+	elseif (APPLE)
+		set(AJA_LIBRARIES_INCLUDE_DIRS
+			${AJA_LIBRARIES_INCLUDE_DIRS}
+			${AJA_LIBRARIES_INCLUDE_DIR}/ajantv2/src/mac)
+	elseif(UNIX AND NOT APPLE)
+		set(AJA_LIBRARIES_INCLUDE_DIRS
+			${AJA_LIBRARIES_INCLUDE_DIRS}
+			${AJA_LIBRARIES_INCLUDE_DIR}/ajantv2/src/lin)
+	endif()
+
+	set(LIBAJANTV2_LIBRARIES ${AJA_NTV2_LIB})
+	if(AJA_NTV2_DEBUG_LIB STREQUAL "AJA_NTV2_DEBUG_LIB-NOTFOUND")
+		set(AJA_NTV2_DEBUG_LIB ${AJA_NTV2_LIB})
+	endif()
+	set(LIBAJANTV2_DEBUG_LIBRARIES ${AJA_NTV2_DEBUG_LIB})
+	set(LIBAJANTV2_INCLUDE_DIRS ${AJA_LIBRARIES_INCLUDE_DIRS})
+endif()

+ 1 - 0
formatcode.sh

@@ -38,5 +38,6 @@ find . -type d \( -path ./deps \
 -o -path ./plugins/mac-syphon/syphon-framework \
 -o -path ./plugins/obs-outputs/ftl-sdk \
 -o -path ./plugins/obs-vst \
+-o -path ./plugins/aja/sdk \
 -o -path ./build \) -prune -type f -o -name '*.h' -or -name '*.hpp' -or -name '*.m' -or -name '*.mm' -or -name '*.c' -or -name '*.cpp' \
 | xargs -L100 -P${NPROC} ${CLANG_FORMAT} -i -style=file  -fallback-style=none

+ 1 - 0
plugins/CMakeLists.txt

@@ -91,3 +91,4 @@ add_subdirectory(obs-transitions)
 add_subdirectory(obs-text)
 add_subdirectory(rtmp-services)
 add_subdirectory(text-freetype2)
+add_subdirectory(aja)

+ 102 - 0
plugins/aja/CMakeLists.txt

@@ -0,0 +1,102 @@
+project(aja)
+
+if(DISABLE_AJA)
+	message(STATUS "aja plugin disabled")
+	return()
+endif()
+
+if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+	find_package(LibAJANTV2)
+	if (NOT LIBAJANTV2_FOUND)
+		message(STATUS "aja plugin disabled (deps not found)")
+		return()
+	else()
+		message("aja plugin includes: ${LIBAJANTV2_INCLUDE_DIRS}")
+		message("aja plugin libs: ${LIBAJANTV2_LIBRARIES}")
+		message("aja plugin debug libs: ${LIBAJANTV2_DEBUG_LIBRARIES}")
+	endif()
+else()
+	message(STATUS "aja plugin disabled (32-bit not supported)")
+	return()
+endif()
+
+set(aja_INCLUDE_DIRS
+	${LIBAJANTV2_INCLUDE_DIRS})
+set(aja_LIBRARIES
+	$<IF:$<CONFIG:Debug>,${LIBAJANTV2_DEBUG_LIBRARIES},${LIBAJANTV2_LIBRARIES}>
+	libobs)
+
+set(aja_SOURCES
+	main.cpp
+	aja-card-manager.cpp
+	aja-common.cpp
+	aja-output.cpp
+	aja-props.cpp
+	aja-routing.cpp
+	aja-source.cpp
+	aja-widget-io.cpp)
+
+set(aja_HEADERS
+	aja-card-manager.hpp
+	aja-common.hpp
+	aja-enums.hpp
+	aja-ui-props.hpp
+	aja-output.hpp
+	aja-props.hpp
+	aja-routing.hpp
+	aja-source.hpp
+	aja-widget-io.hpp)
+
+# macOS
+if(APPLE)
+	set(aja_COMPILE_DEFS
+		AJAMac
+		AJA_MAC)
+
+	find_library(IOKIT_FRAMEWORK Iokit)
+	find_library(COREFOUNDATION_LIBRARY CoreFoundation)
+	find_library(APPKIT_FRAMEWORK AppKit)
+
+	list(APPEND aja_LIBRARIES
+		${IOKIT_FRAMEWORK}
+		${COREFOUNDATION_LIBRARY}
+		${APPKIT_FRAMEWORK})
+# Windows
+elseif(WIN32)
+	set(aja_COMPILE_DEFS
+		AJA_WINDOWS
+		_WINDOWS
+		WIN32
+		MSWindows)
+
+	if(CMAKE_BUILD_TYPE STREQUAL Debug)
+		list(APPEND aja_COMPILE_DEFS
+			_DEBUG)
+	else()
+		list(APPEND aja_COMPILE_DEFS
+			NDEBUG)
+	endif()
+
+	list(APPEND aja_LIBRARIES
+		ws2_32.lib
+		setupapi.lib
+		Winmm.lib
+		netapi32.lib
+		Shlwapi.lib)
+# Linux
+elseif(UNIX AND NOT APPLE)
+	set(aja_COMPILE_DEFS
+		AJA_LINUX
+		AJALinux)
+endif()
+
+add_library(aja MODULE ${aja_SOURCES} ${aja_HEADERS})
+
+target_include_directories(aja PUBLIC ${aja_INCLUDE_DIRS})
+target_link_libraries(aja PUBLIC ${aja_LIBRARIES})
+target_compile_definitions(aja PUBLIC ${aja_COMPILE_DEFS})
+
+set_target_properties(aja PROPERTIES FOLDER
+		"plugins")
+
+install_obs_plugin_with_data(aja data)

+ 78 - 0
plugins/aja/README.md

@@ -0,0 +1,78 @@
+# AJA I/O Device Capture and Output Plugin for OBS
+
+A capture and outptu plugin for OBS for use with AJA NTV2 I/O devices. The plugin communicates with the AJA device driver via the open-source AJA NTV2 SDK (MIT license). See instructions below for installing the AJA device driver.
+
+---
+
+## AJA Device Driver Installation
+
+Usage of this plugin with an AJA I/O device requires installation of the AJA Device Driver.
+
+Follow one of the methods below to install the AJA device driver for your platform.
+
+### Method 1 - AJA Software Installer (Windows/macOS/Ubuntu Linux)
+
+Pre-built driver packages for Windows, macOS and Ubuntu Linux are provided as a component of the AJA Software Installer. Navigate to the AJA Support page and download the latest AJA Software Installer for your platform:
+
+https://www.aja.com/support
+
+### Method 2 - Build From Sources (Linux)
+
+The Linux verison of the AJA device driver can be built from the MIT-licensed AJA NTV2 SDK sources found here: 
+
+https://github.com/aja-video/ntv2
+
+- Clone the ntv2 repository.
+- `cd` into the `ntv2/ajadriver/linux` directory
+- Run `make`
+- `cd` into the `ntv2/bin` directory and run `sudo sh load_ajantv2` to install the AJA kernel module.
+
+NOTE: To uniunstall the Linux kernel module, `cd` into the `ntv2/bin` directory and run `sudo sh unload_ajantv2`.
+
+---
+
+## How to use the plugin
+- Capture plugin instances can be added from `Sources -> Add -> AJA I/O Device Capture`.
+
+- The Output plugin is accessible from `Tools -> AJA I/O Device Output`.
+
+## Updating the plugin AJA NTV2 SDK code
+
+The AJA NTV2 SDK is open-source and licensed under the MIT license. A copy of the SDK is included in the source tree of the OBS AJA plugin directory.
+
+To update the plugin's working copy of the AJA NTV2 SDK:
+
+1. Clone the AJA NTV2 SDK from https://github.com/aja-video/ntv2
+1. Place the contents of the ntv2 directory into `plugins/aja/sdk` within the cloned OBS repository.
+
+You should end up with a directory structure like:
+
+```
+plugins/aja/sdk
+    /ajadriver
+    /ajalibraries
+    /cmake
+    /CMakeLists.txt
+    /LICENSE
+    /README.md
+```
+
+## Known Issues/TODOS
+1. Fix AV sync in the AJA I/O Output plugin with certain OBS/device frame rate combinations.
+1. Improve frame tearing in the AJA I/O Device Capture plugin with certain OBS/device frame rate combinations.
+1. Capture plugins not always auto-detecting signal properly after disconnecting and reconnecting the physical SDI/HDMI cable from the device.
+1. Improve handling of certain invalid video/pixel/SDI 4K transport setting combinations.
+1. Optimize framebuffer memory usage on the device.
+1. Add support for Analog I/O capture and output on supported devices.
+1. Luminance is too bright/washed out in output plugin video.
+1. Certain combinations of video format with RGB pixel format needs additional testing.
+1. 4K video formats and SDI 4K transports need more testing in general.
+1. 8K video not yet implemented.
+1. Add support for selecting an alternate audio input source on supported devices (i.e. Microphone Input on AJA io4K+, analog audio, AES audio, etc). Capture and output currently default to using up to 8 channels of embedded SDI/HDMI audio.
+1. All translation strings are placeholders and default to English. Translation strings needed for additional language support.
+
+---
+
+_Development Credits_
+- ddrboxman
+- paulh-aja

+ 610 - 0
plugins/aja/aja-card-manager.cpp

@@ -0,0 +1,610 @@
+#include "aja-card-manager.hpp"
+#include "aja-common.hpp"
+#include "aja-output.hpp"
+#include "aja-source.hpp"
+
+#include "obs-properties.h"
+
+#include <util/base.h>
+
+#include <ajantv2/includes/ntv2card.h>
+#include <ajantv2/includes/ntv2devicescanner.h>
+#include <ajantv2/includes/ntv2devicefeatures.h>
+#include <ajantv2/includes/ntv2utils.h>
+
+#include <ajabase/system/process.h>
+#include <ajabase/system/systemtime.h>
+
+static const uint32_t kStreamingAppID = NTV2_FOURCC('O', 'B', 'S', ' ');
+
+namespace aja {
+
+CardManager &CardManager::Instance()
+{
+	static CardManager instance;
+	return instance;
+}
+
+CardEntry::CardEntry(uint32_t cardIndex, const std::string &cardID)
+	: mCardIndex{cardIndex},
+	  mCardID{cardID},
+	  mCard{std::make_unique<CNTV2Card>((UWord)cardIndex)},
+	  mChannelPwnz{},
+	  mMutex{}
+{
+}
+
+CardEntry::~CardEntry()
+{
+	if (mCard) {
+		mCard->Close();
+		mCard.reset();
+	}
+}
+
+CNTV2Card *CardEntry::GetCard()
+{
+	return mCard.get();
+}
+
+bool CardEntry::Initialize()
+{
+	if (!mCard) {
+		blog(LOG_ERROR, "Invalid card instance %s!", mCardID.c_str());
+		return false;
+	}
+
+	const NTV2DeviceID deviceID = mCard->GetDeviceID();
+
+	// Briefly enter Standard Tasks mode to reset card via AJA Daemon/Agent.
+	auto taskMode = NTV2_STANDARD_TASKS;
+	mCard->GetEveryFrameServices(taskMode);
+	if (taskMode != NTV2_STANDARD_TASKS) {
+		mCard->SetEveryFrameServices(NTV2_STANDARD_TASKS);
+		AJATime::Sleep(100);
+	}
+
+	mCard->SetEveryFrameServices(NTV2_OEM_TASKS);
+
+	const int32_t obsPid = (int32_t)AJAProcess::GetPid();
+	mCard->AcquireStreamForApplicationWithReference((ULWord)kStreamingAppID,
+							obsPid);
+
+	mCard->SetSuspendHostAudio(true);
+
+	mCard->ClearRouting();
+
+	if (NTV2DeviceCanDoMultiFormat(deviceID)) {
+		mCard->SetMultiFormatMode(true);
+	}
+
+	mCard->SetReference(NTV2_REFERENCE_FREERUN);
+
+	for (UWord i = 0; i < aja::CardNumAudioSystems(deviceID); i++) {
+		mCard->SetAudioLoopBack(NTV2_AUDIO_LOOPBACK_OFF,
+					static_cast<NTV2AudioSystem>(i));
+	}
+
+	auto numFramestores = aja::CardNumFramestores(deviceID);
+
+	for (UWord i = 0; i < NTV2DeviceGetNumVideoInputs(deviceID); i++) {
+		mCard->SetInputFrame(static_cast<NTV2Channel>(i), 0xff);
+		// Disable 3G Level B converter by default
+		if (NTV2DeviceCanDo3GLevelConversion(deviceID)) {
+			mCard->SetSDIInLevelBtoLevelAConversion(
+				static_cast<NTV2Channel>(i), false);
+		}
+	}
+
+	// SDI Outputs Default State
+	for (UWord i = 0; i < NTV2DeviceGetNumVideoOutputs(deviceID); i++) {
+		auto channel = GetNTV2ChannelForIndex(i);
+		if (NTV2DeviceCanDo3GOut(deviceID, i)) {
+			mCard->SetSDIOut3GEnable(channel, true);
+			mCard->SetSDIOut3GbEnable(channel, false);
+		}
+		if (NTV2DeviceCanDo12GOut(deviceID, i)) {
+			mCard->SetSDIOut6GEnable(channel, false);
+			mCard->SetSDIOut12GEnable(channel, false);
+		}
+		if (NTV2DeviceCanDo3GLevelConversion(deviceID)) {
+			mCard->SetSDIOutLevelAtoLevelBConversion(i, false);
+			mCard->SetSDIOutRGBLevelAConversion(i, false);
+		}
+	}
+
+	for (UWord i = 0; i < numFramestores; i++) {
+		auto channel = GetNTV2ChannelForIndex(i);
+
+		if (isAutoCirculateRunning(channel)) {
+			mCard->AutoCirculateStop(channel, true);
+		}
+
+		mCard->SetVideoFormat(NTV2_FORMAT_1080p_5994_A, false, false,
+				      channel);
+		mCard->SetFrameBufferFormat(channel, NTV2_FBF_8BIT_YCBCR);
+
+		mCard->DisableChannel(channel);
+	}
+
+	blog(LOG_DEBUG, "NTV2 Card Initialized: %s", mCardID.c_str());
+
+	return true;
+}
+
+uint32_t CardEntry::GetCardIndex() const
+{
+	return mCardIndex;
+}
+
+std::string CardEntry::GetCardID() const
+{
+	return mCardID;
+}
+
+std::string CardEntry::GetDisplayName() const
+{
+	if (mCard) {
+		std::ostringstream oss;
+		oss << mCard->GetIndexNumber() << " - "
+		    << mCard->GetModelName();
+		const std::string &serial = GetSerial();
+		if (!serial.empty())
+			oss << " (" << serial << ")";
+
+		return oss.str();
+	}
+
+	// very bad if we get here...
+	return "Unknown";
+}
+
+std::string CardEntry::GetSerial() const
+{
+	std::string serial;
+	if (mCard)
+		mCard->GetSerialNumberString(serial);
+
+	return serial;
+}
+
+NTV2DeviceID CardEntry::GetDeviceID() const
+{
+	NTV2DeviceID id = DEVICE_ID_NOTFOUND;
+	if (mCard)
+		id = mCard->GetDeviceID();
+
+	return id;
+}
+
+bool CardEntry::ChannelReady(NTV2Channel chan, const std::string &owner) const
+{
+	const std::lock_guard<std::mutex> lock(mMutex);
+
+	bool found = false;
+
+	for (const auto &pwn : mChannelPwnz) {
+		if (pwn.second & (1 << static_cast<int32_t>(chan))) {
+			return pwn.first == owner;
+		}
+	}
+
+	return !found;
+}
+
+bool CardEntry::AcquireChannel(NTV2Channel chan, NTV2Mode mode,
+			       const std::string &owner)
+{
+	bool acquired = false;
+
+	if (ChannelReady(chan, owner)) {
+		const std::lock_guard<std::mutex> lock(mMutex);
+
+		if (mChannelPwnz.find(owner) != mChannelPwnz.end()) {
+			if (mChannelPwnz[owner] &
+			    (1 << static_cast<int32_t>(chan))) {
+				acquired = true;
+			} else {
+				mChannelPwnz[owner] |=
+					(1 << static_cast<int32_t>(chan));
+				acquired = true;
+			}
+		} else {
+			mChannelPwnz[owner] |=
+				(1 << static_cast<int32_t>(chan));
+			acquired = true;
+		}
+
+		// Acquire interrupt handles
+		if (acquired && mCard) {
+			if (mode == NTV2_MODE_CAPTURE) {
+				mCard->EnableInputInterrupt(chan);
+				mCard->SubscribeInputVerticalEvent(chan);
+			} else if (mode == NTV2_MODE_DISPLAY) {
+				mCard->EnableOutputInterrupt(chan);
+				mCard->SubscribeOutputVerticalEvent(chan);
+			}
+		}
+	}
+
+	return acquired;
+}
+
+bool CardEntry::ReleaseChannel(NTV2Channel chan, NTV2Mode mode,
+			       const std::string &owner)
+{
+	const std::lock_guard<std::mutex> lock(mMutex);
+
+	for (const auto &pwn : mChannelPwnz) {
+		if (pwn.first == owner) {
+			if (mChannelPwnz[owner] &
+			    (1 << static_cast<int32_t>(chan))) {
+				mChannelPwnz[owner] ^=
+					(1 << static_cast<int32_t>(chan));
+
+				// Release interrupt handles
+				if (mCard) {
+					if (mode == NTV2_MODE_CAPTURE) {
+						mCard->DisableInputInterrupt(
+							chan);
+						mCard->UnsubscribeInputVerticalEvent(
+							chan);
+					} else if (mode == NTV2_MODE_DISPLAY) {
+						mCard->DisableOutputInterrupt(
+							chan);
+						mCard->UnsubscribeOutputVerticalEvent(
+							chan);
+					}
+				}
+
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+bool CardEntry::InputSelectionReady(IOSelection io, NTV2DeviceID id,
+				    const std::string &owner) const
+{
+	UNUSED_PARAMETER(id);
+
+	NTV2InputSourceSet inputSources;
+	aja::IOSelectionToInputSources(io, inputSources);
+
+	if (inputSources.size() > 0) {
+		size_t channelsReady = 0;
+
+		for (auto &&src : inputSources) {
+			auto channel = NTV2InputSourceToChannel(src);
+			if (ChannelReady(channel, owner))
+				channelsReady++;
+		}
+
+		if (channelsReady == inputSources.size())
+			return true;
+	}
+
+	return false;
+}
+
+bool CardEntry::OutputSelectionReady(IOSelection io, NTV2DeviceID id,
+				     const std::string &owner) const
+{
+	/* Handle checking special case outputs before all other outputs.
+	 * 1. HDMI Monitor uses framestore 4
+	 * 2. SDI Monitor on Io 4K/Io 4K Plus, etc. uses framestore 4.
+	 * 3. Everything else...
+	 */
+	if (aja::CardCanDoHDMIMonitorOutput(id) &&
+	    io == IOSelection::HDMIMonitorOut) {
+		NTV2Channel hdmiMonChannel = NTV2_CHANNEL4;
+		return ChannelReady(hdmiMonChannel, owner);
+	} else if (aja::CardCanDoSDIMonitorOutput(id) &&
+		   io == IOSelection::SDI5) {
+		NTV2Channel sdiMonChannel = NTV2_CHANNEL4;
+		return ChannelReady(sdiMonChannel, owner);
+	} else {
+		NTV2OutputDestinations outputDests;
+		aja::IOSelectionToOutputDests(io, outputDests);
+		if (outputDests.size() > 0) {
+			size_t channelsReady = 0;
+
+			for (auto &&dst : outputDests) {
+				auto channel =
+					NTV2OutputDestinationToChannel(dst);
+				if (ChannelReady(channel, owner))
+					channelsReady++;
+			}
+
+			if (channelsReady == outputDests.size())
+				return true;
+		}
+	}
+
+	return false;
+}
+
+bool CardEntry::AcquireInputSelection(IOSelection io, NTV2DeviceID id,
+				      const std::string &owner)
+{
+	UNUSED_PARAMETER(id);
+
+	NTV2InputSourceSet inputSources;
+	aja::IOSelectionToInputSources(io, inputSources);
+
+	std::vector<NTV2Channel> acquiredChannels;
+	for (auto &&src : inputSources) {
+		auto acqChan = NTV2InputSourceToChannel(src);
+		if (AcquireChannel(acqChan, NTV2_MODE_CAPTURE, owner)) {
+			blog(LOG_DEBUG, "Source %s acquired channel %s",
+			     owner.c_str(),
+			     NTV2ChannelToString(acqChan).c_str());
+			acquiredChannels.push_back(acqChan);
+		} else {
+			blog(LOG_DEBUG,
+			     "Source %s could not acquire channel %s",
+			     owner.c_str(),
+			     NTV2ChannelToString(acqChan).c_str());
+		}
+	}
+
+	// Release channels if we couldn't acquire all required channels.
+	if (acquiredChannels.size() != inputSources.size()) {
+		for (auto &&ac : acquiredChannels) {
+			ReleaseChannel(ac, NTV2_MODE_CAPTURE, owner);
+		}
+	}
+
+	return acquiredChannels.size() == inputSources.size();
+}
+
+bool CardEntry::ReleaseInputSelection(IOSelection io, NTV2DeviceID id,
+				      const std::string &owner)
+{
+	UNUSED_PARAMETER(id);
+
+	NTV2InputSourceSet currentInputSources;
+	aja::IOSelectionToInputSources(io, currentInputSources);
+	uint32_t releasedCount = 0;
+	for (auto &&src : currentInputSources) {
+		auto relChan = NTV2InputSourceToChannel(src);
+		if (ReleaseChannel(relChan, NTV2_MODE_CAPTURE, owner)) {
+			blog(LOG_DEBUG, "Released Channel %s",
+			     NTV2ChannelToString(relChan).c_str());
+			releasedCount++;
+		}
+	}
+	return releasedCount == currentInputSources.size();
+}
+
+bool CardEntry::AcquireOutputSelection(IOSelection io, NTV2DeviceID id,
+				       const std::string &owner)
+{
+	std::vector<NTV2Channel> acquiredChannels;
+	NTV2OutputDestinations outputDests;
+	aja::IOSelectionToOutputDests(io, outputDests);
+
+	// Handle acquiring special case outputs --
+	// HDMI Monitor uses framestore 4
+	if (aja::CardCanDoHDMIMonitorOutput(id) &&
+	    io == IOSelection::HDMIMonitorOut) {
+		NTV2Channel hdmiMonChannel = NTV2_CHANNEL4;
+		if (AcquireChannel(hdmiMonChannel, NTV2_MODE_DISPLAY, owner)) {
+			blog(LOG_DEBUG, "Output %s acquired channel %s",
+			     owner.c_str(),
+			     NTV2ChannelToString(hdmiMonChannel).c_str());
+			acquiredChannels.push_back(hdmiMonChannel);
+		} else {
+			blog(LOG_DEBUG,
+			     "Output %s could not acquire channel %s",
+			     owner.c_str(),
+			     NTV2ChannelToString(hdmiMonChannel).c_str());
+		}
+	} else if (aja::CardCanDoSDIMonitorOutput(id) &&
+		   io == IOSelection::SDI5) {
+		// SDI Monitor on io4K/io4K+/etc. uses framestore 4
+		NTV2Channel sdiMonChannel = NTV2_CHANNEL4;
+		if (AcquireChannel(sdiMonChannel, NTV2_MODE_DISPLAY, owner)) {
+			blog(LOG_DEBUG, "Output %s acquired channel %s",
+			     owner.c_str(),
+			     NTV2ChannelToString(sdiMonChannel).c_str());
+			acquiredChannels.push_back(sdiMonChannel);
+		} else {
+			blog(LOG_DEBUG,
+			     "Output %s could not acquire channel %s",
+			     owner.c_str(),
+			     NTV2ChannelToString(sdiMonChannel).c_str());
+		}
+	} else {
+		// Handle acquiring all other channels
+		for (auto &&dst : outputDests) {
+			auto acqChan = NTV2OutputDestinationToChannel(dst);
+			if (AcquireChannel(acqChan, NTV2_MODE_DISPLAY, owner)) {
+				acquiredChannels.push_back(acqChan);
+				blog(LOG_DEBUG, "Output %s acquired channel %s",
+				     owner.c_str(),
+				     NTV2ChannelToString(acqChan).c_str());
+			} else {
+				blog(LOG_DEBUG,
+				     "Output %s could not acquire channel %s",
+				     owner.c_str(),
+				     NTV2ChannelToString(acqChan).c_str());
+			}
+		}
+
+		// Release channels if we couldn't acquire all required channels.
+		if (acquiredChannels.size() != outputDests.size()) {
+			for (auto &&ac : acquiredChannels) {
+				ReleaseChannel(ac, NTV2_MODE_DISPLAY, owner);
+			}
+		}
+	}
+
+	return acquiredChannels.size() == outputDests.size();
+}
+
+bool CardEntry::ReleaseOutputSelection(IOSelection io, NTV2DeviceID id,
+				       const std::string &owner)
+{
+	NTV2OutputDestinations currentOutputDests;
+	aja::IOSelectionToOutputDests(io, currentOutputDests);
+	uint32_t releasedCount = 0;
+
+	// Handle releasing special case outputs --
+	// HDMI Monitor uses framestore 4
+	if (aja::CardCanDoHDMIMonitorOutput(id) &&
+	    io == IOSelection::HDMIMonitorOut) {
+		NTV2Channel hdmiMonChannel = NTV2_CHANNEL4;
+		if (ReleaseChannel(hdmiMonChannel, NTV2_MODE_DISPLAY, owner)) {
+			blog(LOG_DEBUG, "Released Channel %s",
+			     NTV2ChannelToString(hdmiMonChannel).c_str());
+			releasedCount++;
+		}
+	} else if (aja::CardCanDoSDIMonitorOutput(id) &&
+		   io == IOSelection::SDI5) {
+		// SDI Monitor on io4K/io4K+/etc. uses framestore 4
+		NTV2Channel sdiMonChannel = NTV2_CHANNEL4;
+		if (ReleaseChannel(sdiMonChannel, NTV2_MODE_DISPLAY, owner)) {
+			blog(LOG_DEBUG, "Released Channel %s",
+			     NTV2ChannelToString(sdiMonChannel).c_str());
+			releasedCount++;
+		}
+	} else {
+		// Release all other channels
+		for (auto &&dst : currentOutputDests) {
+			auto relChan = NTV2OutputDestinationToChannel(dst);
+			if (ReleaseChannel(relChan, NTV2_MODE_DISPLAY, owner)) {
+				blog(LOG_DEBUG, "Released Channel %s",
+				     NTV2ChannelToString(relChan).c_str());
+				releasedCount++;
+			}
+		}
+	}
+	return releasedCount == currentOutputDests.size();
+}
+
+bool CardEntry::UpdateChannelOwnerName(const std::string &oldName,
+				       const std::string &newName)
+{
+	const std::lock_guard<std::mutex> lock(mMutex);
+
+	for (const auto &pwn : mChannelPwnz) {
+		if (pwn.first == oldName) {
+			mChannelPwnz.insert(std::pair<std::string, int32_t>{
+				newName, pwn.second});
+
+			mChannelPwnz.erase(oldName);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+bool CardEntry::isAutoCirculateRunning(NTV2Channel chan)
+{
+	if (!mCard)
+		return false;
+
+	AUTOCIRCULATE_STATUS acStatus;
+	if (mCard->AutoCirculateGetStatus(chan, acStatus)) {
+		if (acStatus.acState != NTV2_AUTOCIRCULATE_RUNNING &&
+		    acStatus.acState != NTV2_AUTOCIRCULATE_STARTING &&
+		    acStatus.acState != NTV2_AUTOCIRCULATE_PAUSED &&
+		    acStatus.acState != NTV2_AUTOCIRCULATE_INIT) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
+void CardManager::ClearCardEntries()
+{
+	const std::lock_guard<std::mutex> lock(mMutex);
+
+	for (auto &&entry : mCardEntries) {
+		CNTV2Card *card = entry.second->GetCard();
+		if (card) {
+			card->SetSuspendHostAudio(false);
+			card->SetEveryFrameServices(NTV2_STANDARD_TASKS);
+			// Workaround for AJA internal bug #11378
+			// Set HDMI output back to Audio System 1 on card release
+			if (NTV2DeviceGetNumHDMIVideoOutputs(
+				    card->GetDeviceID()) > 0) {
+				card->SetHDMIOutAudioSource8Channel(
+					NTV2_AudioChannel1_8,
+					NTV2_AUDIOSYSTEM_1);
+			}
+
+			int32_t pid = (int32_t)AJAProcess::GetPid();
+			card->ReleaseStreamForApplicationWithReference(
+				(ULWord)kStreamingAppID, pid);
+		}
+	}
+
+	mCardEntries.clear();
+}
+
+void CardManager::EnumerateCards()
+{
+	const std::lock_guard<std::mutex> lock(mMutex);
+
+	CNTV2DeviceScanner scanner;
+	for (const auto &iter : scanner.GetDeviceInfoList()) {
+		CNTV2Card card((UWord)iter.deviceIndex);
+		const std::string &cardID = aja::MakeCardID(card);
+
+		// New Card Entry
+		if (mCardEntries.find(cardID) == mCardEntries.end()) {
+			CardEntryPtr cardEntry = std::make_shared<CardEntry>(
+				iter.deviceIndex, cardID);
+			if (cardEntry && cardEntry->Initialize())
+				mCardEntries.emplace(cardID, cardEntry);
+		} else {
+			// Card fell off of the bus and came back with a new physical index?
+			auto currEntry = mCardEntries[cardID];
+			if (currEntry) {
+				if (currEntry->GetCardIndex() !=
+				    iter.deviceIndex) {
+					mCardEntries.erase(cardID);
+					CardEntryPtr cardEntry =
+						std::make_shared<CardEntry>(
+							iter.deviceIndex,
+							cardID);
+					if (cardEntry &&
+					    cardEntry->Initialize())
+						mCardEntries.emplace(cardID,
+								     cardEntry);
+				}
+			}
+		}
+	}
+}
+
+size_t CardManager::NumCardEntries() const
+{
+	const std::lock_guard<std::mutex> lock(mMutex);
+	return mCardEntries.size();
+}
+
+const CardEntryPtr CardManager::GetCardEntry(const std::string &cardID) const
+{
+	const std::lock_guard<std::mutex> lock(mMutex);
+	for (const auto &entry : mCardEntries) {
+		if (entry.second && entry.second->GetCardID() == cardID)
+			return entry.second;
+	}
+	return nullptr;
+}
+
+const CardEntries &CardManager::GetCardEntries() const
+{
+	const std::lock_guard<std::mutex> lock(mMutex);
+	return mCardEntries;
+}
+
+} // aja

+ 103 - 0
plugins/aja/aja-card-manager.hpp

@@ -0,0 +1,103 @@
+#pragma once
+
+#include "aja-props.hpp"
+
+#include <obs-module.h>
+
+#include <ajantv2/includes/ntv2enums.h>
+#include <ajantv2/includes/ntv2publicinterface.h>
+
+#include <memory>
+#include <map>
+#include <mutex>
+#include <vector>
+
+class CNTV2Card;
+class AJAOutput;
+class AJASource;
+
+namespace aja {
+
+using ChannelPwnz = std::map<std::string, int32_t>;
+
+/* A CardEntry for each physical AJA card is added to a map retained by the CardManager.
+ * Each CardEntry itself maintains a map representing the AJA card "Channels" the are
+ * owned by a particular capture or output plugin instance. The Channel ownership map is
+ * then used to determine which "IOSelection" (i.e. SDI1, SDI3+4, HDMI Monitor Output, etc.)
+ * drop-down menu items are either accessible or grayed out in the capture and output plugin UIs.
+ */
+class CardEntry {
+public:
+	CardEntry(uint32_t cardIndex, const std::string &cardID);
+	virtual ~CardEntry();
+	CNTV2Card *GetCard();
+	virtual bool Initialize();
+	virtual uint32_t GetCardIndex() const;
+	virtual std::string GetCardID() const;
+	virtual std::string GetDisplayName() const;
+	virtual std::string GetSerial() const;
+	virtual NTV2DeviceID GetDeviceID() const;
+	virtual bool ChannelReady(NTV2Channel chan,
+				  const std::string &owner) const;
+	virtual bool AcquireChannel(NTV2Channel chan, NTV2Mode mode,
+				    const std::string &owner);
+	virtual bool ReleaseChannel(NTV2Channel chan, NTV2Mode mode,
+				    const std::string &owner);
+	virtual bool InputSelectionReady(IOSelection io, NTV2DeviceID id,
+					 const std::string &owner) const;
+	virtual bool OutputSelectionReady(IOSelection io, NTV2DeviceID id,
+					  const std::string &owner) const;
+	virtual bool AcquireInputSelection(IOSelection io, NTV2DeviceID id,
+					   const std::string &owner);
+	virtual bool ReleaseInputSelection(IOSelection io, NTV2DeviceID id,
+					   const std::string &owner);
+	virtual bool AcquireOutputSelection(IOSelection io, NTV2DeviceID id,
+					    const std::string &owner);
+	virtual bool ReleaseOutputSelection(IOSelection io, NTV2DeviceID id,
+					    const std::string &owner);
+	virtual bool UpdateChannelOwnerName(const std::string &oldName,
+					    const std::string &newName);
+
+private:
+	virtual bool isAutoCirculateRunning(NTV2Channel);
+
+protected:
+	uint32_t mCardIndex;
+	std::string mCardID;
+	std::unique_ptr<CNTV2Card> mCard;
+	ChannelPwnz mChannelPwnz;
+	mutable std::mutex mMutex;
+};
+using CardEntryPtr = std::shared_ptr<CardEntry>;
+using CardEntries = std::map<std::string, CardEntryPtr>;
+
+/* The CardManager enumerates the physical AJA cards in the system, reverts them to a default
+ * state on exit, and maintains a map of CardEntry objects corresponding to each physical card.
+ * Each CardEntry object holds a pointer to the CNTV2Card instance and a map of NTV2Channels
+ * that are "owned" by each plugin instance. NTV2Channels are essentially treated as indices
+ * for various firwmare Widgets and sub-systems throughout the AJA NTV2 SDK.
+ */
+class CardManager {
+public:
+	static CardManager &Instance();
+
+	void ClearCardEntries();
+	void EnumerateCards();
+
+	size_t NumCardEntries() const;
+	const CardEntryPtr GetCardEntry(const std::string &cardID) const;
+	const CardEntries &GetCardEntries() const;
+
+private:
+	CardManager() = default;
+	~CardManager() = default;
+	CardManager(const CardManager &) = delete;
+	CardManager(const CardManager &&) = delete;
+	CardManager &operator=(const CardManager &) = delete;
+	CardManager &operator=(const CardManager &&) = delete;
+
+	CardEntries mCardEntries;
+	mutable std::mutex mMutex;
+};
+
+} // aja

+ 892 - 0
plugins/aja/aja-common.cpp

@@ -0,0 +1,892 @@
+#include "aja-card-manager.hpp"
+#include "aja-common.hpp"
+#include "aja-ui-props.hpp"
+#include "aja-props.hpp"
+
+#include <ajantv2/includes/ntv2devicescanner.h>
+#include <ajantv2/includes/ntv2devicefeatures.h>
+#include <ajantv2/includes/ntv2utils.h>
+
+void filter_io_selection_input_list(const std::string &cardID,
+				    const std::string &channelOwner,
+				    obs_property_t *list)
+{
+	auto &cardManager = aja::CardManager::Instance();
+
+	auto cardEntry = cardManager.GetCardEntry(cardID);
+	if (!cardEntry) {
+		blog(LOG_DEBUG,
+		     "filter_io_selection_input_list: Card Entry not found for %s",
+		     cardID.c_str());
+		return;
+	}
+
+	NTV2DeviceID deviceID = DEVICE_ID_NOTFOUND;
+	CNTV2Card *card = cardEntry->GetCard();
+	if (card)
+		deviceID = card->GetDeviceID();
+
+	// Gray out the IOSelection list items that are in use by other plugin instances
+	for (size_t idx = 0; idx < obs_property_list_item_count(list); idx++) {
+		auto io_select = static_cast<IOSelection>(
+			obs_property_list_item_int(list, idx));
+
+		if (io_select == IOSelection::Invalid) {
+			obs_property_list_item_disable(list, idx, false);
+			continue;
+		}
+
+		bool enabled = cardEntry->InputSelectionReady(
+			io_select, deviceID, channelOwner);
+		obs_property_list_item_disable(list, idx, !enabled);
+		blog(LOG_DEBUG, "IOSelection %s = %s",
+		     aja::IOSelectionToString(io_select).c_str(),
+		     enabled ? "enabled" : "disabled");
+	}
+}
+
+void filter_io_selection_output_list(const std::string &cardID,
+				     const std::string &channelOwner,
+				     obs_property_t *list)
+{
+	auto &cardManager = aja::CardManager::Instance();
+
+	auto cardEntry = cardManager.GetCardEntry(cardID);
+	if (!cardEntry) {
+		blog(LOG_DEBUG,
+		     "filter_io_selection_output_list: Card Entry not found for %s",
+		     cardID.c_str());
+		return;
+	}
+
+	NTV2DeviceID deviceID = DEVICE_ID_NOTFOUND;
+	CNTV2Card *card = cardEntry->GetCard();
+	if (card)
+		deviceID = card->GetDeviceID();
+
+	// Gray out the IOSelection list items that are in use by other plugin instances
+	for (size_t idx = 0; idx < obs_property_list_item_count(list); idx++) {
+		auto io_select = static_cast<IOSelection>(
+			obs_property_list_item_int(list, idx));
+		if (io_select == IOSelection::Invalid) {
+			obs_property_list_item_disable(list, idx, false);
+			continue;
+		}
+
+		bool enabled = cardEntry->OutputSelectionReady(
+			io_select, deviceID, channelOwner);
+		obs_property_list_item_disable(list, idx, !enabled);
+		blog(LOG_DEBUG, "IOSelection %s = %s",
+		     aja::IOSelectionToString(io_select).c_str(),
+		     enabled ? "enabled" : "disabled");
+	}
+}
+
+void populate_io_selection_input_list(const std::string &cardID,
+				      const std::string &channelOwner,
+				      NTV2DeviceID deviceID,
+				      obs_property_t *list)
+{
+	obs_property_list_clear(list);
+
+	obs_property_list_add_int(list, obs_module_text(kUIPropIOSelect.text),
+				  static_cast<long long>(IOSelection::Invalid));
+
+	for (auto i = 0; i < static_cast<int32_t>(IOSelection::NumIOSelections);
+	     i++) {
+		auto ioSelect = static_cast<IOSelection>(i);
+		if (ioSelect == IOSelection::SDI1_2_Squares ||
+		    ioSelect == IOSelection::SDI3_4_Squares)
+			continue;
+
+		if (aja::DeviceCanDoIOSelectionIn(deviceID, ioSelect)) {
+			obs_property_list_add_int(
+				list,
+				aja::IOSelectionToString(ioSelect).c_str(),
+				static_cast<long long>(ioSelect));
+		}
+	}
+
+	filter_io_selection_input_list(cardID, channelOwner, list);
+}
+
+void populate_io_selection_output_list(const std::string &cardID,
+				       const std::string &channelOwner,
+				       NTV2DeviceID deviceID,
+				       obs_property_t *list)
+{
+	obs_property_list_clear(list);
+
+	obs_property_list_add_int(list, obs_module_text(kUIPropIOSelect.text),
+				  static_cast<long long>(IOSelection::Invalid));
+
+	if (deviceID == DEVICE_ID_TTAP_PRO) {
+		obs_property_list_add_int(
+			list, "SDI & HDMI",
+			static_cast<long long>(IOSelection::HDMIMonitorOut));
+	} else {
+		for (auto i = 0;
+		     i < static_cast<int32_t>(IOSelection::NumIOSelections);
+		     i++) {
+			auto ioSelect = static_cast<IOSelection>(i);
+
+			if (ioSelect == IOSelection::Invalid ||
+			    ioSelect == IOSelection::SDI1_2_Squares ||
+			    ioSelect == IOSelection::SDI3_4_Squares)
+				continue;
+
+			if (aja::DeviceCanDoIOSelectionOut(deviceID,
+							   ioSelect)) {
+				obs_property_list_add_int(
+					list,
+					aja::IOSelectionToString(ioSelect)
+						.c_str(),
+					static_cast<long long>(ioSelect));
+			}
+		}
+	}
+
+	filter_io_selection_output_list(cardID, channelOwner, list);
+}
+
+void populate_video_format_list(NTV2DeviceID deviceID, obs_property_t *list,
+				NTV2VideoFormat genlockFormat)
+{
+	VideoFormatList videoFormats = {};
+	VideoStandardList orderedStandards = {};
+	orderedStandards.push_back(NTV2_STANDARD_525);
+	orderedStandards.push_back(NTV2_STANDARD_625);
+	if (NTV2DeviceCanDoHDVideo(deviceID)) {
+		orderedStandards.push_back(NTV2_STANDARD_720);
+		orderedStandards.push_back(NTV2_STANDARD_1080);
+		orderedStandards.push_back(NTV2_STANDARD_1080p);
+	}
+	if (NTV2DeviceCanDo2KVideo(deviceID)) {
+		orderedStandards.push_back(NTV2_STANDARD_2K);
+		orderedStandards.push_back(NTV2_STANDARD_2Kx1080p);
+		orderedStandards.push_back(NTV2_STANDARD_2Kx1080i);
+	}
+	if (NTV2DeviceCanDo4KVideo(deviceID)) {
+		orderedStandards.push_back(NTV2_STANDARD_3840i);
+		orderedStandards.push_back(NTV2_STANDARD_3840x2160p);
+		orderedStandards.push_back(NTV2_STANDARD_3840HFR);
+		orderedStandards.push_back(NTV2_STANDARD_4096i);
+		orderedStandards.push_back(NTV2_STANDARD_4096x2160p);
+		orderedStandards.push_back(NTV2_STANDARD_4096HFR);
+	}
+
+	aja::GetSortedVideoFormats(deviceID, orderedStandards, videoFormats);
+	for (const auto &vf : videoFormats) {
+		bool addFormat = true;
+
+		// Filter formats by framerate family if specified
+		if (genlockFormat != NTV2_FORMAT_UNKNOWN)
+			addFormat = IsMultiFormatCompatible(genlockFormat, vf);
+
+		if (addFormat) {
+			std::string name = NTV2VideoFormatToString(vf, true);
+			obs_property_list_add_int(list, name.c_str(), (int)vf);
+		}
+	}
+}
+
+void populate_pixel_format_list(NTV2DeviceID deviceID, obs_property_t *list)
+{
+	const NTV2PixelFormat supported_pix_fmts[] = {kDefaultAJAPixelFormat,
+						      NTV2_FBF_24BIT_BGR};
+
+	for (auto &&pf : supported_pix_fmts) {
+		if (NTV2DeviceCanDoFrameBufferFormat(deviceID, pf)) {
+			obs_property_list_add_int(
+				list,
+				NTV2FrameBufferFormatToString(pf, true).c_str(),
+				static_cast<long long>(pf));
+		}
+	}
+}
+
+void populate_sdi_4k_transport_list(obs_property_t *list)
+{
+	obs_property_list_add_int(
+		list,
+		aja::SDI4KTransportToString(SDI4KTransport::Squares).c_str(),
+		static_cast<long long>(SDI4KTransport::Squares));
+	obs_property_list_add_int(
+		list,
+		aja::SDI4KTransportToString(SDI4KTransport::TwoSampleInterleave)
+			.c_str(),
+		static_cast<long long>(SDI4KTransport::TwoSampleInterleave));
+}
+
+bool aja_video_format_changed(obs_properties_t *props, obs_property_t *list,
+			      obs_data_t *settings)
+{
+	UNUSED_PARAMETER(list);
+
+	auto vid_fmt = static_cast<NTV2VideoFormat>(
+		obs_data_get_int(settings, kUIPropVideoFormatSelect.id));
+
+	obs_property_t *sdi_4k_trx =
+		obs_properties_get(props, kUIPropSDI4KTransport.id);
+
+	obs_property_set_visible(sdi_4k_trx, NTV2_IS_4K_VIDEO_FORMAT(vid_fmt));
+
+	return true;
+}
+
+namespace aja {
+
+video_format AJAPixelFormatToOBSVideoFormat(NTV2PixelFormat pf)
+{
+	video_format obs_video_format = VIDEO_FORMAT_NONE;
+	switch (pf) {
+	case NTV2_FBF_8BIT_YCBCR:
+		obs_video_format = VIDEO_FORMAT_UYVY;
+		break;
+	case NTV2_FBF_24BIT_RGB:
+	case NTV2_FBF_24BIT_BGR:
+		obs_video_format = VIDEO_FORMAT_BGR3;
+		break;
+	case NTV2_FBF_ARGB:
+	case NTV2_FBF_ABGR:
+	case NTV2_FBF_RGBA:
+		obs_video_format = VIDEO_FORMAT_BGRA;
+		break;
+	case NTV2_FBF_10BIT_YCBCR:
+	case NTV2_FBF_10BIT_RGB:
+	case NTV2_FBF_8BIT_YCBCR_YUY2:
+	case NTV2_FBF_10BIT_DPX:
+	case NTV2_FBF_10BIT_YCBCR_DPX:
+	case NTV2_FBF_8BIT_DVCPRO:
+	case NTV2_FBF_8BIT_YCBCR_420PL3:
+	case NTV2_FBF_8BIT_HDV:
+	case NTV2_FBF_10BIT_YCBCRA:
+	case NTV2_FBF_10BIT_DPX_LE:
+	case NTV2_FBF_48BIT_RGB:
+	case NTV2_FBF_12BIT_RGB_PACKED:
+	case NTV2_FBF_PRORES_DVCPRO:
+	case NTV2_FBF_PRORES_HDV:
+	case NTV2_FBF_10BIT_RGB_PACKED:
+	case NTV2_FBF_10BIT_ARGB:
+	case NTV2_FBF_16BIT_ARGB:
+	case NTV2_FBF_8BIT_YCBCR_422PL3:
+	case NTV2_FBF_10BIT_RAW_RGB:
+	case NTV2_FBF_10BIT_RAW_YCBCR:
+	case NTV2_FBF_10BIT_YCBCR_420PL3_LE:
+	case NTV2_FBF_10BIT_YCBCR_422PL3_LE:
+	case NTV2_FBF_10BIT_YCBCR_420PL2:
+	case NTV2_FBF_10BIT_YCBCR_422PL2:
+	case NTV2_FBF_8BIT_YCBCR_420PL2:
+	case NTV2_FBF_8BIT_YCBCR_422PL2:
+	default:
+		obs_video_format = VIDEO_FORMAT_NONE;
+		break;
+	}
+
+	return obs_video_format;
+}
+
+void GetSortedVideoFormats(NTV2DeviceID id, const VideoStandardList &standards,
+			   VideoFormatList &videoFormats)
+{
+	if (standards.empty())
+		return;
+
+	VideoFormatMap videoFormatMap;
+
+	// Bin all the formats based on video standard
+	for (size_t i = (size_t)NTV2_FORMAT_UNKNOWN;
+	     i < (size_t)NTV2_MAX_NUM_VIDEO_FORMATS; i++) {
+		NTV2VideoFormat fmt = (NTV2VideoFormat)i;
+		NTV2Standard standard = GetNTV2StandardFromVideoFormat(fmt);
+
+		bool addFmt = true;
+
+		if (id != DEVICE_ID_NOTFOUND) {
+			addFmt = NTV2DeviceCanDoVideoFormat(id, fmt);
+		}
+
+		if (addFmt) {
+			if (videoFormatMap.count(standard)) {
+				videoFormatMap.at(standard).push_back(fmt);
+			} else {
+				std::vector<NTV2VideoFormat> v;
+
+				v.push_back(fmt);
+
+				videoFormatMap.insert(
+					std::pair<NTV2Standard,
+						  std::vector<NTV2VideoFormat>>(
+						standard, v));
+			}
+		}
+	}
+
+	for (size_t v = (size_t)NTV2_STANDARD_1080;
+	     v < (size_t)NTV2_NUM_STANDARDS; v++) {
+		NTV2Standard standard = (NTV2Standard)v;
+
+		if (videoFormatMap.count(standard)) {
+			std::sort(videoFormatMap.at(standard).begin(),
+				  videoFormatMap.at(standard).end(),
+				  [&](const NTV2VideoFormat &d1,
+				      const NTV2VideoFormat &d2) {
+					  std::string d1Str, d2Str;
+
+					  d1Str = NTV2VideoFormatToString(d1);
+					  d2Str = NTV2VideoFormatToString(d2);
+
+					  return d1Str < d2Str;
+				  });
+		}
+	}
+
+	for (size_t v = 0; v < standards.size(); v++) {
+		NTV2Standard standard = standards.at(v);
+		if (videoFormatMap.count(standard)) {
+			for (size_t i = 0;
+			     i < videoFormatMap.at(standard).size(); i++) {
+				NTV2VideoFormat vf =
+					videoFormatMap.at(standard).at(i);
+				videoFormats.push_back(vf);
+			}
+		}
+	}
+}
+
+uint32_t CardNumFramestores(NTV2DeviceID id)
+{
+	auto numFramestores = NTV2DeviceGetNumFrameStores(id);
+	if (id == DEVICE_ID_CORVIDHBR) {
+		numFramestores = 1;
+	}
+	return numFramestores;
+}
+
+uint32_t CardNumAudioSystems(NTV2DeviceID id)
+{
+	if (id == DEVICE_ID_KONALHI || id == DEVICE_ID_KONALHEPLUS)
+		return 2;
+
+	return NTV2DeviceGetNumAudioSystems(id);
+}
+
+// IO4K and IO4K+ perform SDI Monitor Output on "SDI5" and "Framestore 4".
+bool CardCanDoSDIMonitorOutput(NTV2DeviceID id)
+{
+	return (id == DEVICE_ID_IO4K || id == DEVICE_ID_IO4KPLUS);
+}
+
+// Cards with a dedicated HDMI Monitor Output tie it to "Framestore 4".
+bool CardCanDoHDMIMonitorOutput(NTV2DeviceID id)
+{
+	return (id == DEVICE_ID_IO4K || id == DEVICE_ID_IO4KPLUS ||
+		id == DEVICE_ID_IOXT || id == DEVICE_ID_IOX3 ||
+		id == DEVICE_ID_KONA4 || id == DEVICE_ID_KONA5 ||
+		id == DEVICE_ID_KONA5_8K || id == DEVICE_ID_KONA5_2X4K ||
+		id == DEVICE_ID_KONA5_8KMK);
+}
+
+// Cards capable of 1x SDI at 6G/12G.
+bool CardCanDo1xSDI12G(NTV2DeviceID id)
+{
+	return (id == DEVICE_ID_KONA5_8K || id == DEVICE_ID_KONA5_8KMK ||
+		id == DEVICE_ID_KONA5 || id == DEVICE_ID_KONA5_2X4K ||
+		id == DEVICE_ID_IO4KPLUS || id == DEVICE_ID_CORVID44_12G);
+}
+
+// Check for 3G level-B SDI on the wire.
+bool Is3GLevelB(CNTV2Card *card, NTV2Channel channel)
+{
+	if (!card)
+		return false;
+
+	bool levelB = false;
+	auto deviceID = card->GetDeviceID();
+	UWord channelIndex = static_cast<UWord>(channel);
+
+	if (NTV2DeviceCanDo3GIn(deviceID, channelIndex) ||
+	    NTV2DeviceCanDo12GIn(deviceID, channelIndex)) {
+		if (!card->GetSDIInput3GbPresent(levelB, channel))
+			return false;
+	}
+
+	return levelB;
+}
+
+// Get the 3G Level-A enum for a 3G Level-B format enum.
+NTV2VideoFormat GetLevelAFormatForLevelBFormat(NTV2VideoFormat vf)
+{
+	NTV2VideoFormat result = vf;
+	switch (vf) {
+	default:
+		break;
+	case NTV2_FORMAT_1080p_5000_B:
+		result = NTV2_FORMAT_1080p_5000_A;
+		break;
+	case NTV2_FORMAT_1080p_5994_B:
+		result = NTV2_FORMAT_1080p_5994_A;
+		break;
+	case NTV2_FORMAT_1080p_6000_B:
+		result = NTV2_FORMAT_1080p_6000_A;
+		break;
+	case NTV2_FORMAT_1080p_2K_4795_B:
+		result = NTV2_FORMAT_1080p_2K_4795_A;
+		break;
+	case NTV2_FORMAT_1080p_2K_4800_B:
+		result = NTV2_FORMAT_1080p_2K_4800_A;
+		break;
+	case NTV2_FORMAT_1080p_2K_5000_B:
+		result = NTV2_FORMAT_1080p_2K_5000_A;
+		break;
+	case NTV2_FORMAT_1080p_2K_5994_B:
+		result = NTV2_FORMAT_1080p_2K_5994_A;
+		break;
+	case NTV2_FORMAT_1080p_2K_6000_B:
+		result = NTV2_FORMAT_1080p_2K_6000_A;
+		break;
+	}
+	return result;
+}
+
+NTV2VideoFormat InterlacedFormatForPsfFormat(NTV2VideoFormat vf)
+{
+	NTV2VideoFormat result = vf;
+	switch (vf) {
+	default:
+		break;
+	case NTV2_FORMAT_1080psf_2500_2:
+		result = NTV2_FORMAT_1080i_5000;
+		break;
+	case NTV2_FORMAT_1080psf_2997_2:
+		result = NTV2_FORMAT_1080i_5994;
+		break;
+	}
+	return result;
+}
+
+// Certain cards only have 1 SDI spigot.
+bool IsSingleSDIDevice(NTV2DeviceID id)
+{
+	return (id == DEVICE_ID_TTAP_PRO || id == DEVICE_ID_KONA1);
+}
+
+bool IsIODevice(NTV2DeviceID id)
+{
+	return (id == DEVICE_ID_IOXT || id == DEVICE_ID_IOX3 ||
+		id == DEVICE_ID_IO4K || id == DEVICE_ID_IO4KPLUS ||
+		id == DEVICE_ID_IOIP_2022 || id == DEVICE_ID_IOIP_2110);
+}
+
+bool IsRetailSDI12G(NTV2DeviceID id)
+{
+	return (id == DEVICE_ID_KONA5 || id == DEVICE_ID_IO4KPLUS);
+}
+
+bool IsOutputOnlyDevice(NTV2DeviceID id)
+{
+	return id == DEVICE_ID_TTAP_PRO;
+}
+
+std::string SDI4KTransportToString(SDI4KTransport mode)
+{
+	std::string str = "";
+	switch (mode) {
+	case SDI4KTransport::Squares:
+		str = "Squares";
+		break;
+	case SDI4KTransport::TwoSampleInterleave:
+		str = "2SI";
+		break;
+	default:
+	case SDI4KTransport::Unknown:
+		str = "Unknown";
+		break;
+	}
+	return str;
+}
+
+std::string IOSelectionToString(IOSelection io)
+{
+	std::string str;
+
+	switch (io) {
+	case IOSelection::SDI1:
+		str = "SDI 1";
+		break;
+	case IOSelection::SDI2:
+		str = "SDI 2";
+		break;
+	case IOSelection::SDI3:
+		str = "SDI 3";
+		break;
+	case IOSelection::SDI4:
+		str = "SDI 4";
+		break;
+	case IOSelection::SDI5:
+		str = "SDI 5";
+		break;
+	case IOSelection::SDI6:
+		str = "SDI 6";
+		break;
+	case IOSelection::SDI7:
+		str = "SDI 7";
+		break;
+	case IOSelection::SDI8:
+		str = "SDI 8";
+		break;
+	case IOSelection::SDI1_2:
+		str = "SDI 1 & 2";
+		break;
+	case IOSelection::SDI1_2_Squares:
+		str = "SDI 1 & 2 (4K Squares)";
+		break;
+	case IOSelection::SDI3_4:
+		str = "SDI 3 & 4";
+		break;
+	case IOSelection::SDI3_4_Squares:
+		str = "SDI 3 & 4 (4K Squares)";
+		break;
+	case IOSelection::SDI5_6:
+		str = "SDI 5 & 6";
+		break;
+	case IOSelection::SDI7_8:
+		str = "SDI 7 & 8";
+		break;
+	case IOSelection::SDI1__4:
+		str = "SDI 1-4";
+		break;
+	case IOSelection::SDI5__8:
+		str = "SDI 5-8";
+		break;
+	case IOSelection::HDMI1:
+		str = "HDMI 1";
+		break;
+	case IOSelection::HDMI2:
+		str = "HDMI 2";
+		break;
+	case IOSelection::HDMI3:
+		str = "HDMI 3";
+		break;
+	case IOSelection::HDMI4:
+		str = "HDMI 4";
+		break;
+	case IOSelection::HDMIMonitorIn:
+		str = "HDMI Monitor In";
+		break;
+	case IOSelection::HDMIMonitorOut:
+		str = "HDMI Monitor Out";
+		break;
+	case IOSelection::AnalogIn:
+		str = "Analog In";
+		break;
+	case IOSelection::AnalogOut:
+		str = "Analog Out";
+		break;
+	case IOSelection::Invalid:
+		str = "Invalid";
+		break;
+	}
+
+	return str;
+}
+
+void IOSelectionToInputSources(IOSelection io, NTV2InputSourceSet &inputSources)
+{
+	switch (io) {
+	case IOSelection::SDI1:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI1);
+		break;
+	case IOSelection::SDI2:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI2);
+		break;
+	case IOSelection::SDI3:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI3);
+		break;
+	case IOSelection::SDI4:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI4);
+		break;
+	case IOSelection::SDI5:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI5);
+		break;
+	case IOSelection::SDI6:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI6);
+		break;
+	case IOSelection::SDI7:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI7);
+		break;
+	case IOSelection::SDI8:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI8);
+		break;
+	case IOSelection::SDI1_2:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI1);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI2);
+		break;
+	case IOSelection::SDI1_2_Squares:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI1);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI2);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI3);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI4);
+		break;
+	case IOSelection::SDI3_4:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI3);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI4);
+		break;
+	case IOSelection::SDI3_4_Squares:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI1);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI2);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI3);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI4);
+		break;
+	case IOSelection::SDI5_6:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI5);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI6);
+		break;
+	case IOSelection::SDI7_8:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI7);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI8);
+		break;
+	case IOSelection::SDI1__4:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI1);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI2);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI3);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI4);
+		break;
+	case IOSelection::SDI5__8:
+		inputSources.insert(NTV2_INPUTSOURCE_SDI5);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI6);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI7);
+		inputSources.insert(NTV2_INPUTSOURCE_SDI8);
+		break;
+	case IOSelection::HDMI1:
+		inputSources.insert(NTV2_INPUTSOURCE_HDMI1);
+		break;
+	case IOSelection::HDMI2:
+		inputSources.insert(NTV2_INPUTSOURCE_HDMI2);
+		break;
+	case IOSelection::HDMI3:
+		inputSources.insert(NTV2_INPUTSOURCE_HDMI3);
+		break;
+	case IOSelection::HDMI4:
+		inputSources.insert(NTV2_INPUTSOURCE_HDMI4);
+		break;
+	case IOSelection::HDMIMonitorIn:
+		inputSources.insert(NTV2_INPUTSOURCE_HDMI1);
+		break;
+	case IOSelection::AnalogIn:
+		inputSources.insert(NTV2_INPUTSOURCE_ANALOG1);
+		break;
+	default:
+	case IOSelection::HDMIMonitorOut:
+	case IOSelection::AnalogOut:
+	case IOSelection::Invalid:
+		break;
+	}
+}
+
+void IOSelectionToOutputDests(IOSelection io,
+			      NTV2OutputDestinations &outputDests)
+{
+	switch (io) {
+	case IOSelection::SDI1:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI1);
+		break;
+	case IOSelection::SDI2:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI2);
+		break;
+	case IOSelection::SDI3:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI3);
+		break;
+	case IOSelection::SDI4:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI4);
+		break;
+	case IOSelection::SDI5:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI5);
+		break;
+	case IOSelection::SDI6:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI6);
+		break;
+	case IOSelection::SDI7:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI7);
+		break;
+	case IOSelection::SDI8:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI8);
+		break;
+	case IOSelection::SDI1_2:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI1);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI2);
+		break;
+	// Requires 4x framestores and 2x SDI spigots
+	case IOSelection::SDI1_2_Squares:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI1);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI2);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI3);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI4);
+		break;
+	case IOSelection::SDI3_4:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI3);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI4);
+		break;
+	// Requires 4x framestores and 2x SDI spigots
+	case IOSelection::SDI3_4_Squares:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI1);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI2);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI3);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI4);
+		break;
+	case IOSelection::SDI5_6:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI5);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI6);
+		break;
+	case IOSelection::SDI7_8:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI7);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI8);
+		break;
+	case IOSelection::SDI1__4:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI1);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI2);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI3);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI4);
+		break;
+	case IOSelection::SDI5__8:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI5);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI6);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI7);
+		outputDests.insert(NTV2_OUTPUTDESTINATION_SDI8);
+		break;
+	case IOSelection::HDMIMonitorOut:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_HDMI);
+		break;
+	case IOSelection::AnalogOut:
+		outputDests.insert(NTV2_OUTPUTDESTINATION_ANALOG);
+		break;
+	default:
+	case IOSelection::HDMI1:
+	case IOSelection::HDMI2:
+	case IOSelection::HDMI3:
+	case IOSelection::HDMI4:
+	case IOSelection::HDMIMonitorIn:
+	case IOSelection::AnalogIn:
+	case IOSelection::Invalid:
+		break;
+	}
+}
+
+bool DeviceCanDoIOSelectionIn(NTV2DeviceID id, IOSelection io)
+{
+	NTV2InputSourceSet inputSources;
+	if (io != IOSelection::Invalid) {
+		IOSelectionToInputSources(io, inputSources);
+		size_t numSrcs = inputSources.size();
+		size_t canDo = 0;
+		if (numSrcs > 0) {
+			for (auto &&inp : inputSources) {
+				if (NTV2DeviceCanDoInputSource(id, inp))
+					canDo++;
+			}
+
+			if (canDo == numSrcs)
+				return true;
+		}
+	}
+	return false;
+}
+
+bool DeviceCanDoIOSelectionOut(NTV2DeviceID id, IOSelection io)
+{
+	NTV2OutputDestinations outputDests;
+	if (io != IOSelection::Invalid) {
+		IOSelectionToOutputDests(io, outputDests);
+		size_t numOuts = outputDests.size();
+		size_t canDo = 0;
+		if (numOuts > 0) {
+			for (auto &&out : outputDests) {
+				if (NTV2DeviceCanDoOutputDestination(id, out))
+					canDo++;
+			}
+
+			if (canDo == numOuts)
+				return true;
+		}
+	}
+	return false;
+}
+
+bool IsSDIOneWireIOSelection(IOSelection io)
+{
+	bool result = false;
+	switch (io) {
+	case IOSelection::SDI1:
+	case IOSelection::SDI2:
+	case IOSelection::SDI3:
+	case IOSelection::SDI4:
+	case IOSelection::SDI5:
+	case IOSelection::SDI6:
+	case IOSelection::SDI7:
+	case IOSelection::SDI8:
+		result = true;
+		break;
+	default:
+		result = false;
+	}
+	return result;
+}
+
+bool IsSDITwoWireIOSelection(IOSelection io)
+{
+	bool result = false;
+	switch (io) {
+	case IOSelection::SDI1_2:
+	case IOSelection::SDI1_2_Squares:
+	case IOSelection::SDI3_4:
+	case IOSelection::SDI3_4_Squares:
+	case IOSelection::SDI5_6:
+	case IOSelection::SDI7_8:
+		result = true;
+		break;
+	default:
+		result = false;
+	}
+	return result;
+}
+
+bool IsSDIFourWireIOSelection(IOSelection io)
+{
+	bool result = false;
+	switch (io) {
+	case IOSelection::SDI1__4:
+	case IOSelection::SDI5__8:
+		result = true;
+		break;
+	default:
+		result = false;
+	}
+	return result;
+}
+
+bool IsMonitorOutputSelection(NTV2DeviceID id, IOSelection io)
+{
+	if (CardCanDoSDIMonitorOutput(id) && io == IOSelection::SDI5)
+		return true;
+
+	if (CardCanDoHDMIMonitorOutput(id) && io == IOSelection::HDMIMonitorOut)
+		return true;
+
+	return false;
+}
+
+std::string MakeCardID(CNTV2Card &card)
+{
+	std::string cardID;
+	if (card.GetSerialNumberString(cardID)) {
+		// Try to construct CardID from device ID and serial number...
+		cardID = NTV2DeviceIDToString(card.GetDeviceID(), false) + "_" +
+			 cardID;
+	} else {
+		// ...otherwise fall back to the CNTV2DeviceScanner method.
+		cardID = CNTV2DeviceScanner::GetDeviceRefName(card);
+	}
+	return cardID;
+}
+
+} // aja

+ 92 - 0
plugins/aja/aja-common.hpp

@@ -0,0 +1,92 @@
+#include "aja-enums.hpp"
+
+#include <obs-module.h>
+
+#include <algorithm>
+#include <map>
+#include <string>
+#include <vector>
+
+#include <ajantv2/includes/ntv2enums.h>
+#include <ajantv2/includes/ntv2card.h>
+
+using VideoFormatMap = std::map<NTV2Standard, std::vector<NTV2VideoFormat>>;
+using VideoFormatList = std::vector<NTV2VideoFormat>;
+using VideoStandardList = std::vector<NTV2Standard>;
+
+static const uint32_t kDefaultAudioChannels = 8;
+static const uint32_t kDefaultAudioSampleRate = 48000;
+static const uint32_t kDefaultAudioSampleSize = 4;
+static const int kVideoFormatAuto = -1;
+static const int kPixelFormatAuto = -1;
+static const NTV2PixelFormat kDefaultAJAPixelFormat = NTV2_FBF_8BIT_YCBCR;
+
+// Common OBS property helpers used by both the capture and output plugins
+extern void filter_io_selection_input_list(const std::string &cardID,
+					   const std::string &channelOwner,
+					   obs_property_t *list);
+extern void filter_io_selection_output_list(const std::string &cardID,
+					    const std::string &channelOwner,
+					    obs_property_t *list);
+extern void populate_io_selection_input_list(const std::string &cardID,
+					     const std::string &channelOwner,
+					     NTV2DeviceID deviceID,
+					     obs_property_t *list);
+extern void populate_io_selection_output_list(const std::string &cardID,
+					      const std::string &channelOwner,
+					      NTV2DeviceID deviceID,
+					      obs_property_t *list);
+extern void
+populate_video_format_list(NTV2DeviceID deviceID, obs_property_t *list,
+			   NTV2VideoFormat genlockFormat = NTV2_FORMAT_UNKNOWN);
+extern void populate_pixel_format_list(NTV2DeviceID deviceID,
+				       obs_property_t *list);
+extern void populate_sdi_4k_transport_list(obs_property_t *list);
+extern bool aja_video_format_changed(obs_properties_t *props,
+				     obs_property_t *list,
+				     obs_data_t *settings);
+
+// Additional helpers for AJA channel and signal routing configuration not found in the NTV2 SDK
+namespace aja {
+
+template<typename T> bool vec_contains(const std::vector<T> &vec, const T &elem)
+{
+	return std::find(vec.begin(), vec.end(), elem) != vec.end();
+}
+
+extern video_format AJAPixelFormatToOBSVideoFormat(NTV2PixelFormat pf);
+
+extern void GetSortedVideoFormats(NTV2DeviceID id,
+				  const VideoStandardList &standards,
+				  VideoFormatList &videoFormats);
+
+extern uint32_t CardNumFramestores(NTV2DeviceID id);
+extern uint32_t CardNumAudioSystems(NTV2DeviceID id);
+extern bool CardCanDoSDIMonitorOutput(NTV2DeviceID id);
+extern bool CardCanDoHDMIMonitorOutput(NTV2DeviceID id);
+extern bool CardCanDo1xSDI12G(NTV2DeviceID id);
+extern bool Is3GLevelB(CNTV2Card *card, NTV2Channel channel);
+extern NTV2VideoFormat GetLevelAFormatForLevelBFormat(NTV2VideoFormat vf);
+extern NTV2VideoFormat InterlacedFormatForPsfFormat(NTV2VideoFormat vf);
+extern bool IsSingleSDIDevice(NTV2DeviceID id);
+extern bool IsIODevice(NTV2DeviceID id);
+extern bool IsRetailSDI12G(NTV2DeviceID id);
+extern bool IsOutputOnlyDevice(NTV2DeviceID id);
+
+extern std::string SDI4KTransportToString(SDI4KTransport mode);
+
+extern std::string IOSelectionToString(IOSelection io);
+extern void IOSelectionToInputSources(IOSelection io,
+				      NTV2InputSourceSet &inputSources);
+extern void IOSelectionToOutputDests(IOSelection io,
+				     NTV2OutputDestinations &outputDests);
+extern bool DeviceCanDoIOSelectionIn(NTV2DeviceID id, IOSelection io);
+extern bool DeviceCanDoIOSelectionOut(NTV2DeviceID id, IOSelection io);
+extern bool IsSDIOneWireIOSelection(IOSelection io);
+extern bool IsSDITwoWireIOSelection(IOSelection io);
+extern bool IsSDIFourWireIOSelection(IOSelection io);
+extern bool IsMonitorOutputSelection(NTV2DeviceID id, IOSelection io);
+
+extern std::string MakeCardID(CNTV2Card &card);
+
+} // aja

+ 88 - 0
plugins/aja/aja-enums.hpp

@@ -0,0 +1,88 @@
+#pragma once
+
+// Additional enums used throughout the AJA plugins for signal routing configuration.
+
+enum class IOSelection {
+	SDI1 = 0,
+	SDI2 = 1,
+	SDI3 = 2,
+	SDI4 = 3,
+	SDI5 = 4,
+	SDI6 = 5,
+	SDI7 = 6,
+	SDI8 = 7,
+	SDI1_2 = 8,
+	// special case for 2xSDI 4K Squares (requires 4x framestores)
+	SDI1_2_Squares = 9,
+	SDI3_4 = 10,
+	// special case for 2xSDI 4K Squares (requires 4x framestores)
+	SDI3_4_Squares = 11,
+	SDI5_6 = 12,
+	SDI7_8 = 13,
+	SDI1__4 = 14,
+	SDI5__8 = 15,
+	HDMI1 = 16,
+	HDMI2 = 17,
+	HDMI3 = 18,
+	HDMI4 = 19,
+	HDMIMonitorIn = 20,
+	HDMIMonitorOut = 21,
+	AnalogIn = 22,
+	AnalogOut = 23,
+	Invalid = 24,
+	NumIOSelections = Invalid
+};
+
+enum class SDI4KTransport { Squares = 0, TwoSampleInterleave = 1, Unknown = 2 };
+
+enum class RasterDefinition {
+	SD = 0,
+	HD = 1,
+	UHD_4K = 2,
+	UHD_4K_Retail_12G = 3,
+	UHD2_8K = 4,
+	Unknown = 5
+};
+
+enum class SDIWireFormat {
+	SD_ST352 = 0,
+	HD_720p_ST292 = 1,
+	HD_1080_ST292 = 2,
+	HD_1080_ST372_Dual = 3,
+	HD_720p_ST425_3Ga = 4,
+	HD_1080p_ST425_3Ga = 5,
+	HD_1080p_ST425_3Gb_DL = 6,
+	HD_720p_ST425_3Gb = 7,
+	HD_1080p_ST425_3Gb = 8,
+	HD_1080p_ST425_Dual_3Ga = 9,
+	HD_1080p_ST425_Dual_3Gb = 10,
+	UHD4K_ST292_Dual_1_5_Squares = 11,
+	UHD4K_ST292_Quad_1_5_Squares = 12,
+	UHD4K_ST425_Quad_3Ga_Squares = 13,
+	UHD4K_ST425_Quad_3Gb_Squares = 14,
+	UHD4K_ST425_Dual_3Gb_2SI = 15,
+	UHD4K_ST425_Quad_3Ga_2SI = 16,
+	UHD4K_ST425_Quad_3Gb_2SI = 17,
+	UHD4K_ST2018_6G_Squares_2SI = 18,
+	UHD4K_ST2018_6G_Squares_2SI_Kona5_io4KPlus = 19,
+	UHD4K_ST2018_12G_Squares_2SI = 20,
+	UHD4K_ST2018_12G_Squares_2SI_Kona5_io4KPlus = 21,
+	UHD28K_ST2082_Dual_12G = 22,
+	UHD28K_ST2082_RGB_Dual_12G = 23,
+	UHD28K_ST2082_Quad_12G = 24,
+	Unknown = 25,
+};
+
+enum class HDMIWireFormat {
+	SD_HDMI = 0,
+	HD_YCBCR_LFR = 1,
+	HD_YCBCR_HFR = 2,
+	HD_RGB_LFR = 3,
+	HD_RGB_HFR = 4,
+	UHD_4K_YCBCR_LFR = 5,
+	UHD_4K_YCBCR_HFR = 6,
+	UHD_4K_RGB_LFR = 7,
+	UHD_4K_RGB_HFR = 8,
+	TTAP_PRO = 9,
+	Unknown = 10
+};

+ 1267 - 0
plugins/aja/aja-output.cpp

@@ -0,0 +1,1267 @@
+#include "aja-card-manager.hpp"
+#include "aja-common.hpp"
+#include "aja-ui-props.hpp"
+#include "aja-output.hpp"
+#include "aja-routing.hpp"
+
+#include <obs-module.h>
+#include <util/platform.h>
+
+#include <ajabase/common/timer.h>
+#include <ajabase/system/systemtime.h>
+
+#include <ajantv2/includes/ntv2card.h>
+#include <ajantv2/includes/ntv2devicefeatures.h>
+
+#include <atomic>
+#include <stdlib.h>
+
+// Log AJA Output video/audio delay and av-sync
+// #define AJA_OUTPUT_STATS
+
+static constexpr uint32_t kNumCardFrames = 3;
+static const int64_t kDefaultStatPeriod = 3000000000;
+static const int64_t kAudioSyncAdjust = 20000;
+
+static void copy_audio_data(struct audio_data *src, struct audio_data *dst,
+			    size_t size)
+{
+	if (src->data[0]) {
+		dst->data[0] = (uint8_t *)bmemdup(src->data[0], size);
+	}
+}
+
+static void free_audio_data(struct audio_data *frames)
+{
+	if (frames->data[0]) {
+		bfree(frames->data[0]);
+		frames->data[0] = NULL;
+	}
+	memset(frames, 0, sizeof(*frames));
+}
+
+static void copy_video_data(struct video_data *src, struct video_data *dst,
+			    size_t size)
+{
+	if (src->data[0]) {
+		dst->data[0] = (uint8_t *)bmemdup(src->data[0], size);
+	}
+}
+
+static void free_video_frame(struct video_data *frame)
+{
+	if (frame->data[0]) {
+		bfree(frame->data[0]);
+		frame->data[0] = NULL;
+	}
+
+	memset(frame, 0, sizeof(*frame));
+}
+
+AJAOutput::AJAOutput(CNTV2Card *card, const std::string &cardID,
+		     const std::string &outputID, UWord deviceIndex,
+		     const NTV2DeviceID deviceID)
+	: mCardID{cardID},
+	  mOutputID{outputID},
+	  mDeviceIndex{deviceIndex},
+	  mDeviceID{deviceID},
+	  mFrameTimes{},
+	  mAudioPlayCursor{0},
+	  mAudioWriteCursor{0},
+	  mAudioWrapAddress{0},
+	  mAudioRate{0},
+	  mAudioQueueSamples{0},
+	  mAudioWriteSamples{0},
+	  mAudioPlaySamples{0},
+	  mNumCardFrames{0},
+	  mFirstCardFrame{0},
+	  mLastCardFrame{0},
+	  mWriteCardFrame{0},
+	  mPlayCardFrame{0},
+	  mPlayCardNext{0},
+	  mFrameRateNum{0},
+	  mFrameRateDen{0},
+	  mVideoQueueFrames{0},
+	  mVideoWriteFrames{0},
+	  mVideoPlayFrames{0},
+	  mFirstVideoTS{0},
+	  mFirstAudioTS{0},
+	  mLastVideoTS{0},
+	  mLastAudioTS{0},
+	  mVideoDelay{0},
+	  mAudioDelay{0},
+	  mAudioVideoSync{0},
+	  mAudioAdjust{0},
+	  mLastStatTime{0},
+#ifdef AJA_WRITE_DEBUG_WAV
+	  mWaveWriter{nullptr},
+#endif
+	  mCard{card},
+	  mOutputProps{DEVICE_ID_NOTFOUND},
+	  mTestPattern{},
+	  mIsRunning{false},
+	  mAudioStarted{false},
+	  mRunThread{},
+	  mVideoLock{},
+	  mAudioLock{},
+	  mRunThreadLock{},
+	  mVideoQueue{},
+	  mAudioQueue{},
+	  mOBSOutput{nullptr}
+{
+	mVideoQueue = std::make_unique<VideoQueue>();
+	mAudioQueue = std::make_unique<AudioQueue>();
+}
+
+AJAOutput::~AJAOutput()
+{
+	if (mVideoQueue)
+		mVideoQueue.reset();
+	if (mAudioQueue)
+		mAudioQueue.reset();
+}
+
+CNTV2Card *AJAOutput::GetCard()
+{
+	return mCard;
+}
+
+void AJAOutput::Initialize(const OutputProps &props)
+{
+	const auto &audioSystem = props.AudioSystem();
+
+	// Store the address to the end of the card's audio buffer.
+	mCard->GetAudioWrapAddress(mAudioWrapAddress, audioSystem);
+
+	// Specify the frame indices for the "on-air" frames on the card.
+	// Starts at frame index corresponding to the output Channel * numFrames
+	calculate_card_frame_indices(kNumCardFrames, mCard->GetDeviceID(),
+				     props.Channel(), props.videoFormat,
+				     props.pixelFormat);
+
+	mCard->SetOutputFrame(props.Channel(), mWriteCardFrame);
+
+	mCard->WaitForOutputVerticalInterrupt(props.Channel());
+
+	const auto &cardFrameRate =
+		GetNTV2FrameRateFromVideoFormat(props.videoFormat);
+
+	ULWord fpsNum = 0;
+	ULWord fpsDen = 0;
+	GetFramesPerSecond(cardFrameRate, fpsNum, fpsDen);
+	mFrameRateNum = fpsNum;
+	mFrameRateDen = fpsDen;
+	mFrameTimes.cardFrameTime =
+		(1000000000ULL / (uint64_t)(fpsNum / fpsDen));
+	mFrameTimes.cardFps = (double)(fpsNum / fpsDen);
+	mFrameTimes.obsFps = obs_get_active_fps();
+	if (mFrameTimes.obsFps < 1.0)
+		mFrameTimes.obsFps = 30.0;
+	mFrameTimes.obsFrameTime =
+		(1000000000ULL / (uint64_t)mFrameTimes.obsFps);
+
+	mVideoDelay = ((int64_t)mNumCardFrames - 0) * 1000000 * mFrameRateDen /
+		      mFrameRateNum;
+
+	mAudioRate = props.audioSampleRate;
+
+	SetOutputProps(props);
+}
+
+void AJAOutput::SetOBSOutput(obs_output_t *output)
+{
+	mOBSOutput = output;
+}
+
+obs_output_t *AJAOutput::GetOBSOutput()
+{
+	return mOBSOutput;
+}
+
+void AJAOutput::SetOutputProps(const OutputProps &props)
+{
+	mOutputProps = props;
+}
+
+OutputProps AJAOutput::GetOutputProps() const
+{
+	return mOutputProps;
+}
+
+void AJAOutput::GenerateTestPattern(NTV2VideoFormat vf, NTV2PixelFormat pf,
+				    NTV2TestPatternSelect pattern)
+{
+	NTV2VideoFormat vid_fmt = vf;
+	NTV2PixelFormat pix_fmt = pf;
+
+	if (vid_fmt == NTV2_FORMAT_UNKNOWN)
+		vid_fmt = NTV2_FORMAT_720p_5994;
+	if (pix_fmt == NTV2_FBF_INVALID)
+		pix_fmt = kDefaultAJAPixelFormat;
+
+	NTV2FormatDesc fd(vid_fmt, pix_fmt, NTV2_VANCMODE_OFF);
+	auto bufSize = fd.GetTotalRasterBytes();
+
+	// Raster size changed, regenerate pattern
+	if (bufSize != mTestPattern.size()) {
+		mTestPattern.clear();
+		mTestPattern.resize(bufSize);
+
+		NTV2TestPatternGen gen;
+		gen.DrawTestPattern(pattern, fd.GetRasterWidth(),
+				    fd.GetRasterHeight(), pix_fmt,
+				    mTestPattern);
+	}
+
+	if (mTestPattern.size() == 0) {
+		blog(LOG_DEBUG,
+		     "AJAOutput::GenerateTestPattern: Error generating test pattern!");
+		return;
+	}
+
+	auto outputChannel = mOutputProps.Channel();
+
+	mCard->SetOutputFrame(outputChannel, mWriteCardFrame);
+
+	mCard->DMAWriteFrame(
+		mWriteCardFrame,
+		reinterpret_cast<ULWord *>(&mTestPattern.data()[0]),
+		static_cast<ULWord>(mTestPattern.size()));
+}
+
+void AJAOutput::QueueVideoFrame(struct video_data *frame, size_t size)
+{
+	const std::lock_guard<std::mutex> lock(mVideoLock);
+
+	VideoFrame vf;
+	vf.frame = *frame;
+	vf.frameNum = mVideoWriteFrames;
+	vf.size = size;
+	vf.frame = *frame;
+
+	if (mVideoQueue->size() > kVideoQueueMaxSize) {
+		auto &front = mVideoQueue->front();
+		free_video_frame(&front.frame);
+		mVideoQueue->pop_front();
+	}
+
+	copy_video_data(frame, &vf.frame, size);
+
+	mVideoQueue->push_back(vf);
+	mVideoQueueFrames++;
+}
+
+void AJAOutput::QueueAudioFrames(struct audio_data *frames, size_t size)
+{
+	const std::lock_guard<std::mutex> lock(mAudioLock);
+
+	AudioFrames af;
+	af.frames = *frames;
+	af.offset = 0;
+	af.size = size;
+	af.frames = *frames;
+
+	if (mAudioQueue->size() > kAudioQueueMaxSize) {
+		auto &front = mAudioQueue->front();
+		free_audio_data(&front.frames);
+		mAudioQueue->pop_front();
+	}
+
+	copy_audio_data(frames, &af.frames, size);
+
+	mAudioQueue->push_back(af);
+	mAudioQueueSamples +=
+		size / (kDefaultAudioChannels * kDefaultAudioSampleSize);
+}
+
+void AJAOutput::ClearVideoQueue()
+{
+	const std::lock_guard<std::mutex> lock(mVideoLock);
+	while (mVideoQueue->size() > 0) {
+		auto &vf = mVideoQueue->front();
+		free_video_frame(&vf.frame);
+		mVideoQueue->pop_front();
+	}
+}
+
+void AJAOutput::ClearAudioQueue()
+{
+	const std::lock_guard<std::mutex> lock(mAudioLock);
+	while (mAudioQueue->size() > 0) {
+		auto &af = mAudioQueue->front();
+		free_audio_data(&af.frames);
+		mAudioQueue->pop_front();
+	}
+}
+
+bool AJAOutput::HaveEnoughAudio(size_t needAudioSize)
+{
+	bool ok = false;
+
+	if (mAudioQueue->size() > 0) {
+		size_t available = 0;
+		for (size_t i = 0; i < mAudioQueue->size(); i++) {
+			AudioFrames af = mAudioQueue->at(i);
+			available += af.size - af.offset;
+			if (available >= needAudioSize) {
+				ok = true;
+				break;
+			}
+		}
+	}
+
+	return ok;
+}
+
+size_t AJAOutput::VideoQueueSize()
+{
+	return mVideoQueue->size();
+}
+
+size_t AJAOutput::AudioQueueSize()
+{
+	return mAudioQueue->size();
+}
+
+// lock audio queue before calling
+void AJAOutput::DMAAudioFromQueue(NTV2AudioSystem audioSys)
+{
+	AudioFrames &af = mAudioQueue->front();
+	size_t sizeLeft = af.size - af.offset;
+
+	if (!mFirstAudioTS)
+		mFirstAudioTS = af.frames.timestamp;
+	mLastAudioTS = af.frames.timestamp;
+
+	if (sizeLeft == 0) {
+		free_audio_data(&af.frames);
+		mAudioQueue->pop_front();
+		return;
+	}
+
+	// Get audio play cursor
+	mCard->ReadAudioLastOut(mAudioPlayCursor, audioSys);
+
+	// Calculate audio delay
+	uint32_t audioPlaySamples = 0;
+
+	if (mAudioPlayCursor <= mAudioWriteCursor) {
+		audioPlaySamples =
+			(mAudioWriteCursor - mAudioPlayCursor) /
+			(kDefaultAudioChannels * kDefaultAudioSampleSize);
+	} else {
+		audioPlaySamples =
+			(mAudioWrapAddress - mAudioPlayCursor +
+			 mAudioWriteCursor) /
+			(kDefaultAudioChannels * kDefaultAudioSampleSize);
+	}
+	mAudioDelay = 1000000 * (int64_t)audioPlaySamples / mAudioRate;
+
+	// Adjust audio sync when requested
+	if (mAudioAdjust != 0) {
+		if (mAudioAdjust > 0) {
+			// Throw away some samples to resync audio
+			uint32_t adjustSamples =
+				(uint32_t)mAudioAdjust * mAudioRate / 1000000;
+			uint32_t adjustSize = adjustSamples *
+					      kDefaultAudioSampleSize *
+					      kDefaultAudioChannels;
+			if (adjustSize <= sizeLeft) {
+				af.offset += adjustSize;
+				sizeLeft -= adjustSize;
+				mAudioAdjust = 0;
+				blog(LOG_DEBUG,
+				     "AJAOutput::DMAAudioFromQueue: Drop %d audio samples",
+				     adjustSamples);
+			} else {
+				uint32_t samples =
+					sizeLeft / (kDefaultAudioSampleSize *
+						    kDefaultAudioChannels);
+				af.offset += sizeLeft;
+				sizeLeft = 0;
+				adjustSamples -= samples;
+				mAudioAdjust =
+					adjustSamples * 1000000 / mAudioRate;
+				blog(LOG_DEBUG,
+				     "AJAOutput::DMAAudioFromQueue: Drop %d audio samples",
+				     samples);
+			}
+		} else {
+			// Add some silence to resync audio
+			uint32_t adjustSamples = (uint32_t)(-mAudioAdjust) *
+						 mAudioRate / 1000000;
+			uint32_t adjustSize = adjustSamples *
+					      kDefaultAudioSampleSize *
+					      kDefaultAudioChannels;
+			uint8_t *silentBuffer = new uint8_t[adjustSize];
+			memset(silentBuffer, 0, adjustSize);
+			dma_audio_samples(audioSys, (uint32_t *)silentBuffer,
+					  adjustSize);
+			delete[] silentBuffer;
+			mAudioAdjust = 0;
+			blog(LOG_DEBUG,
+			     "AJAOutput::DMAAudioFromQueue: Add %d audio samples",
+			     adjustSamples);
+		}
+	}
+
+	// Write audio to the hardware ring
+	if (af.frames.data[0] && sizeLeft > 0) {
+		dma_audio_samples(audioSys,
+				  (uint32_t *)&af.frames.data[0][af.offset],
+				  sizeLeft);
+		af.offset += sizeLeft;
+	}
+
+	// Free the audio buffer
+	if (af.offset == af.size) {
+		free_audio_data(&af.frames);
+		mAudioQueue->pop_front();
+	}
+}
+
+// lock video queue before calling
+void AJAOutput::DMAVideoFromQueue()
+{
+	auto &vf = mVideoQueue->front();
+	auto data = vf.frame.data[0];
+
+	if (!mFirstVideoTS)
+		mFirstVideoTS = vf.frame.timestamp;
+	mLastVideoTS = vf.frame.timestamp;
+
+	// find the next buffer
+	uint32_t writeCardFrame = mWriteCardFrame + 1;
+	if (writeCardFrame > mLastCardFrame)
+		writeCardFrame = mFirstCardFrame;
+
+	// use the next buffer if available
+	if (writeCardFrame != mPlayCardFrame)
+		mWriteCardFrame = writeCardFrame;
+
+	mVideoWriteFrames++;
+
+	auto result = mCard->DMAWriteFrame(mWriteCardFrame,
+					   reinterpret_cast<ULWord *>(data),
+					   (ULWord)vf.size);
+	if (!result)
+		blog(LOG_DEBUG,
+		     "AJAOutput::DMAVideoFromQueue: Failed ot write video frame!");
+
+	free_video_frame(&vf.frame);
+	mVideoQueue->pop_front();
+}
+
+// TODO(paulh): Keep track of framebuffer indices used on the card, between the capture
+// and output plugins, so that we can optimize frame index placement in memory and
+// reduce unused gaps in between channel frame indices.
+void AJAOutput::calculate_card_frame_indices(uint32_t numFrames,
+					     NTV2DeviceID id,
+					     NTV2Channel channel,
+					     NTV2VideoFormat vf,
+					     NTV2PixelFormat pf)
+{
+	ULWord channelIndex = GetIndexForNTV2Channel(channel);
+
+	ULWord totalCardFrames = NTV2DeviceGetNumberFrameBuffers(
+		id, GetNTV2FrameGeometryFromVideoFormat(vf), pf);
+
+	mFirstCardFrame = channelIndex * numFrames;
+
+	if (mFirstCardFrame < totalCardFrames &&
+	    (mFirstCardFrame + numFrames) < totalCardFrames) {
+		// Reserve N framebuffers in card DRAM.
+		mNumCardFrames = numFrames;
+		mWriteCardFrame = mFirstCardFrame;
+		mLastCardFrame = mWriteCardFrame + numFrames;
+	} else {
+		// otherwise just grab 2 frames to ping-pong between
+		mNumCardFrames = 2;
+		mWriteCardFrame = channelIndex * 2;
+		mLastCardFrame = mWriteCardFrame + 2;
+	}
+}
+
+uint32_t AJAOutput::get_frame_count()
+{
+	uint32_t frameCount = 0;
+	NTV2Channel channel = mOutputProps.Channel();
+	INTERRUPT_ENUMS interrupt = NTV2ChannelToOutputInterrupt(channel);
+	bool isProgressiveTransport = NTV2_IS_PROGRESSIVE_STANDARD(
+		::GetNTV2StandardFromVideoFormat(mOutputProps.videoFormat));
+
+	if (isProgressiveTransport) {
+		mCard->GetInterruptCount(interrupt, frameCount);
+	} else {
+		uint32_t intCount;
+		uint32_t nextCount;
+		NTV2FieldID fieldID;
+		mCard->GetInterruptCount(interrupt, intCount);
+		mCard->GetOutputFieldID(channel, fieldID);
+		mCard->GetInterruptCount(interrupt, nextCount);
+		if (intCount != nextCount) {
+			mCard->GetInterruptCount(interrupt, intCount);
+			mCard->GetOutputFieldID(channel, fieldID);
+		}
+		if (fieldID == NTV2_FIELD1)
+			intCount--;
+		frameCount = intCount / 2;
+	}
+
+	return frameCount;
+}
+
+// Perform DMA of audio samples to AJA card while taking into account wrapping around the
+// ends of the card's audio buffer (size set to 4MB in Routing::ConfigureOutputAudio).
+void AJAOutput::dma_audio_samples(NTV2AudioSystem audioSys, uint32_t *data,
+				  size_t size)
+{
+	bool result = false;
+
+	mAudioWriteSamples +=
+		size / (kDefaultAudioChannels * kDefaultAudioSampleSize);
+
+	if ((mAudioWriteCursor + size) > mAudioWrapAddress) {
+		const uint32_t remainingBuffer =
+			mAudioWrapAddress - mAudioWriteCursor;
+
+		auto audioDataRemain = reinterpret_cast<const ULWord *>(
+			(uint8_t *)(data) + remainingBuffer);
+
+		// Incoming audio size will wrap around the end of the card audio buffer.
+		// Transfer enough bytes to fill to the end of the buffer...
+		if (remainingBuffer > 0) {
+			result = mCard->DMAWriteAudio(audioSys, data,
+						      mAudioWriteCursor,
+						      remainingBuffer);
+			if (!result) {
+				blog(LOG_DEBUG,
+				     "AJAOutput::dma_audio_samples: "
+				     "failed to write bytes at end of buffer (address = %d)",
+				     mAudioWriteCursor);
+			}
+		}
+
+		// ...transfer remaining bytes at the front of the card audio buffer.
+		if (size - remainingBuffer > 0) {
+			result = mCard->DMAWriteAudio(
+				audioSys, audioDataRemain, 0,
+				(uint32_t)size - remainingBuffer);
+			if (!result) {
+				blog(LOG_DEBUG,
+				     "AJAOutput::dma_audio_samples "
+				     "failed to write bytes at front of buffer (address = %d)",
+				     mAudioWriteCursor);
+			}
+		}
+
+		mAudioWriteCursor = (uint32_t)size - remainingBuffer;
+	} else {
+		//	No wrap, so just do a linear DMA from the buffer...
+		if (size > 0) {
+			result = mCard->DMAWriteAudio(audioSys, data,
+						      mAudioWriteCursor,
+						      (ULWord)size);
+			if (!result) {
+				blog(LOG_DEBUG,
+				     "AJAOutput::dma_audio_samples "
+				     "failed to write bytes to buffer (address = %d)",
+				     mAudioWriteCursor);
+			}
+		}
+
+		mAudioWriteCursor += (uint32_t)size;
+	}
+}
+
+void AJAOutput::CreateThread(bool enable)
+{
+	const std::lock_guard<std::mutex> lock(mRunThreadLock);
+	if (!mRunThread.Active()) {
+		mRunThread.SetPriority(AJA_ThreadPriority_High);
+		mRunThread.SetThreadName("AJA Video Output Thread");
+		mRunThread.Attach(AJAOutput::OutputThread, this);
+	}
+	if (enable) {
+		mIsRunning = true;
+		mRunThread.Start();
+	}
+}
+
+void AJAOutput::StopThread()
+{
+	const std::lock_guard<std::mutex> lock(mRunThreadLock);
+	mIsRunning = false;
+	if (mRunThread.Active()) {
+		mRunThread.Stop();
+	}
+}
+
+bool AJAOutput::ThreadRunning()
+{
+	return mIsRunning;
+}
+
+void AJAOutput::OutputThread(AJAThread *thread, void *ctx)
+{
+	UNUSED_PARAMETER(thread);
+
+	AJAOutput *ajaOutput = static_cast<AJAOutput *>(ctx);
+	if (!ajaOutput) {
+		blog(LOG_ERROR,
+		     "AJAOutput::OutputThread: AJA Output instance is null!");
+		return;
+	}
+
+	CNTV2Card *card = ajaOutput->GetCard();
+	if (!card) {
+		blog(LOG_ERROR,
+		     "AJAOutput::OutputThread: Card instance is null!");
+		return;
+	}
+
+	const auto &props = ajaOutput->GetOutputProps();
+	const auto &audioSystem = props.AudioSystem();
+	uint64_t videoPlayLast = ajaOutput->get_frame_count();
+	uint32_t audioSyncCount = 0;
+	uint32_t videoSyncCount = 0;
+	uint32_t syncCountMax = 5;
+	int64_t audioSyncSum = 0;
+	int64_t videoSyncSum = 0;
+
+	// thread loop
+	while (ajaOutput->ThreadRunning()) {
+		// Wait for preroll
+		if (!ajaOutput->mAudioStarted &&
+		    (ajaOutput->mAudioDelay > ajaOutput->mVideoDelay)) {
+			card->StartAudioOutput(audioSystem, false);
+			ajaOutput->mAudioStarted = true;
+			blog(LOG_DEBUG,
+			     "AJAOutput::OutputThread: Audio Preroll complete");
+		}
+
+		// Check if a vsync occurred
+		uint32_t frameCount = ajaOutput->get_frame_count();
+		if (frameCount > videoPlayLast) {
+			videoPlayLast = frameCount;
+			ajaOutput->mPlayCardFrame = ajaOutput->mPlayCardNext;
+
+			if (ajaOutput->mPlayCardFrame !=
+			    ajaOutput->mWriteCardFrame) {
+				uint32_t playCardNext =
+					ajaOutput->mPlayCardFrame + 1;
+				if (playCardNext > ajaOutput->mLastCardFrame)
+					playCardNext =
+						ajaOutput->mFirstCardFrame;
+
+				if (playCardNext !=
+				    ajaOutput->mWriteCardFrame) {
+					ajaOutput->mPlayCardNext = playCardNext;
+					// Increment the play frame
+					ajaOutput->mCard->SetOutputFrame(
+						ajaOutput->mOutputProps
+							.Channel(),
+						ajaOutput->mPlayCardNext);
+				}
+				ajaOutput->mVideoPlayFrames++;
+			}
+		}
+
+		// Audio DMA
+		{
+			const std::lock_guard<std::mutex> lock(
+				ajaOutput->mAudioLock);
+			while (ajaOutput->AudioQueueSize() > 0) {
+				ajaOutput->DMAAudioFromQueue(audioSystem);
+			}
+		}
+
+		// Video DMA
+		{
+			const std::lock_guard<std::mutex> lock(
+				ajaOutput->mVideoLock);
+			while (ajaOutput->VideoQueueSize() > 0) {
+				ajaOutput->DMAVideoFromQueue();
+			}
+		}
+
+		// Get current time and audio play cursor
+		int64_t curTime = (int64_t)os_gettime_ns();
+		card->ReadAudioLastOut(ajaOutput->mAudioPlayCursor,
+				       audioSystem);
+
+		if (ajaOutput->mAudioStarted &&
+		    ((curTime - ajaOutput->mLastStatTime) >
+		     kDefaultStatPeriod)) {
+			ajaOutput->mLastStatTime = curTime;
+
+			// Calculate av sync delay
+			ajaOutput->mAudioVideoSync =
+				ajaOutput->mAudioDelay - ajaOutput->mVideoDelay;
+
+			if (ajaOutput->mAudioVideoSync > kAudioSyncAdjust) {
+				audioSyncCount++;
+				audioSyncSum += ajaOutput->mAudioVideoSync;
+				if (audioSyncCount >= syncCountMax) {
+					ajaOutput->mAudioAdjust =
+						audioSyncSum / syncCountMax;
+					audioSyncCount = 0;
+					audioSyncSum = 0;
+				}
+			} else {
+				audioSyncCount = 0;
+				audioSyncSum = 0;
+			}
+			if (ajaOutput->mAudioVideoSync < -kAudioSyncAdjust) {
+				videoSyncCount++;
+				videoSyncSum += ajaOutput->mAudioVideoSync;
+				if (videoSyncCount >= syncCountMax) {
+					ajaOutput->mAudioAdjust =
+						videoSyncSum / syncCountMax;
+					videoSyncCount = 0;
+					videoSyncSum = 0;
+				}
+			} else {
+				videoSyncCount = 0;
+				videoSyncSum = 0;
+			}
+
+#ifdef AJA_OUTPUT_STATS
+			blog(LOG_DEBUG,
+			     "AJAOutput::OutputThread: vd %li  ad %li  avs %li",
+			     ajaOutput->mVideoDelay, ajaOutput->mAudioDelay,
+			     ajaOutput->mAudioVideoSync);
+#endif
+		}
+
+		os_sleep_ms(1);
+	}
+
+	ajaOutput->mAudioStarted = false;
+
+	blog(LOG_INFO,
+	     "AJAOutput::OutputThread: Thread stopped. Played %lld video frames",
+	     ajaOutput->mVideoQueueFrames);
+}
+
+void populate_output_device_list(obs_property_t *list)
+{
+	obs_property_list_clear(list);
+
+	auto &cardManager = aja::CardManager::Instance();
+	cardManager.EnumerateCards();
+	for (auto &iter : cardManager.GetCardEntries()) {
+		if (!iter.second)
+			continue;
+
+		CNTV2Card *card = iter.second->GetCard();
+		if (!card)
+			continue;
+
+		NTV2DeviceID deviceID = card->GetDeviceID();
+
+		//TODO(paulh): Add support for analog I/O
+		// w/ NTV2DeviceGetNumAnalogVideoOutputs(cardEntry.deviceID)
+		if (NTV2DeviceGetNumVideoOutputs(deviceID) > 0 ||
+		    NTV2DeviceGetNumHDMIVideoOutputs(deviceID) > 0) {
+
+			obs_property_list_add_string(
+				list, iter.second->GetDisplayName().c_str(),
+				iter.second->GetCardID().c_str());
+		}
+	}
+}
+
+bool aja_output_device_changed(void *data, obs_properties_t *props,
+			       obs_property_t *list, obs_data_t *settings)
+{
+	UNUSED_PARAMETER(data);
+
+	blog(LOG_DEBUG, "AJA Output Device Changed");
+
+	populate_output_device_list(list);
+
+	const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
+	if (!cardID) {
+		blog(LOG_ERROR, "aja_output_device_changed: Card ID is null!");
+		return false;
+	}
+
+	const char *outputID =
+		obs_data_get_string(settings, kUIPropAJAOutputID.id);
+	auto &cardManager = aja::CardManager::Instance();
+	cardManager.EnumerateCards();
+	auto cardEntry = cardManager.GetCardEntry(cardID);
+	if (!cardEntry) {
+		blog(LOG_ERROR,
+		     "aja_output_device_changed: Card Entry not found for %s",
+		     cardID);
+		return false;
+	}
+
+	CNTV2Card *card = cardEntry->GetCard();
+	if (!card) {
+		blog(LOG_ERROR,
+		     "aja_output_device_changed: Card instance is null!");
+		return false;
+	}
+
+	obs_property_t *io_select_list =
+		obs_properties_get(props, kUIPropOutput.id);
+	obs_property_t *vid_fmt_list =
+		obs_properties_get(props, kUIPropVideoFormatSelect.id);
+	obs_property_t *pix_fmt_list =
+		obs_properties_get(props, kUIPropPixelFormatSelect.id);
+	obs_property_t *sdi_4k_list =
+		obs_properties_get(props, kUIPropSDI4KTransport.id);
+
+	const NTV2DeviceID deviceID = cardEntry->GetDeviceID();
+	populate_io_selection_output_list(cardID, outputID, deviceID,
+					  io_select_list);
+
+	// If Channel 1 is actively in use, filter the video format list to only
+	// show video formats within the same framerate family. If Channel 1 is
+	// not active we just go ahead and try to set all framestores to the same video format.
+	// This is because Channel 1's clock rate will govern the card's Free Run clock.
+	NTV2VideoFormat videoFormatChannel1 = NTV2_FORMAT_UNKNOWN;
+	if (!cardEntry->ChannelReady(NTV2_CHANNEL1, outputID)) {
+		card->GetVideoFormat(videoFormatChannel1, NTV2_CHANNEL1);
+	}
+
+	obs_property_list_clear(vid_fmt_list);
+	populate_video_format_list(deviceID, vid_fmt_list, videoFormatChannel1);
+
+	obs_property_list_clear(pix_fmt_list);
+	populate_pixel_format_list(deviceID, pix_fmt_list);
+
+	obs_property_list_clear(sdi_4k_list);
+	populate_sdi_4k_transport_list(sdi_4k_list);
+
+	return true;
+}
+
+bool aja_output_dest_changed(obs_properties_t *props, obs_property_t *list,
+			     obs_data_t *settings)
+{
+	UNUSED_PARAMETER(props);
+
+	blog(LOG_DEBUG, "AJA Output Dest Changed");
+
+	auto &cardManager = aja::CardManager::Instance();
+	cardManager.EnumerateCards();
+	const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
+	if (!cardID) {
+		blog(LOG_ERROR, "aja_output_dest_changed: Card ID is null!");
+		return false;
+	}
+
+	auto cardEntry = cardManager.GetCardEntry(cardID);
+	if (!cardEntry) {
+		blog(LOG_ERROR,
+		     "aja_output_dest_changed: Card entry not found for %s",
+		     cardID);
+		return false;
+	}
+
+	// Revert to "Select..." if desired IOSelection is already in use
+	auto io_select = static_cast<IOSelection>(
+		obs_data_get_int(settings, kUIPropOutput.id));
+	for (size_t i = 0; i < obs_property_list_item_count(list); i++) {
+		auto io_item = static_cast<IOSelection>(
+			obs_property_list_item_int(list, i));
+		if (io_item == io_select &&
+		    obs_property_list_item_disabled(list, i)) {
+			obs_data_set_int(
+				settings, kUIPropOutput.id,
+				static_cast<long long>(IOSelection::Invalid));
+			blog(LOG_WARNING,
+			     "aja_output_dest_changed: IOSelection %s is already in use",
+			     aja::IOSelectionToString(io_select).c_str());
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static void aja_output_destroy(void *data)
+{
+	blog(LOG_DEBUG, "AJA Output Destroy");
+
+	auto ajaOutput = (AJAOutput *)data;
+	if (!ajaOutput) {
+		blog(LOG_ERROR, "aja_output_destroy: Plugin instance is null!");
+		return;
+	}
+
+#ifdef AJA_WRITE_DEBUG_WAV
+	if (ajaOutput->mWaveWriter) {
+		ajaOutput->mWaveWriter->close();
+		delete ajaOutput->mWaveWriter;
+		ajaOutput->mWaveWriter = nullptr;
+	}
+#endif
+	ajaOutput->StopThread();
+	ajaOutput->ClearVideoQueue();
+	ajaOutput->ClearAudioQueue();
+	delete ajaOutput;
+	ajaOutput = nullptr;
+}
+
+static void *aja_output_create(obs_data_t *settings, obs_output_t *output)
+{
+	blog(LOG_INFO, "Creating AJA Output...");
+
+	const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
+	if (!cardID) {
+		blog(LOG_ERROR, "aja_output_create: Card ID is null!");
+		return false;
+	}
+	const char *outputID =
+		obs_data_get_string(settings, kUIPropAJAOutputID.id);
+
+	auto &cardManager = aja::CardManager::Instance();
+	auto cardEntry = cardManager.GetCardEntry(cardID);
+	if (!cardEntry) {
+		blog(LOG_ERROR,
+		     "aja_output_create: Card Entry not found for %s", cardID);
+		return nullptr;
+	}
+
+	CNTV2Card *card = cardEntry->GetCard();
+	if (!card) {
+		blog(LOG_ERROR,
+		     "aja_output_create: Card instance is null for %s", cardID);
+		return nullptr;
+	}
+
+	NTV2DeviceID deviceID = card->GetDeviceID();
+
+	OutputProps outputProps(deviceID);
+	outputProps.ioSelect = static_cast<IOSelection>(
+		obs_data_get_int(settings, kUIPropOutput.id));
+	if (outputProps.ioSelect == IOSelection::Invalid) {
+		blog(LOG_DEBUG,
+		     "aja_output_create: Select a valid AJA Output IOSelection!");
+		return nullptr;
+	}
+
+	outputProps.videoFormat = static_cast<NTV2VideoFormat>(
+		obs_data_get_int(settings, kUIPropVideoFormatSelect.id));
+	outputProps.pixelFormat = static_cast<NTV2PixelFormat>(
+		obs_data_get_int(settings, kUIPropPixelFormatSelect.id));
+	outputProps.sdi4kTransport = static_cast<SDI4KTransport>(
+		obs_data_get_int(settings, kUIPropSDI4KTransport.id));
+
+	outputProps.audioNumChannels = kDefaultAudioChannels;
+	outputProps.audioSampleSize = kDefaultAudioSampleSize;
+	outputProps.audioSampleRate = kDefaultAudioSampleRate;
+
+	if (NTV2_IS_4K_VIDEO_FORMAT(outputProps.videoFormat) &&
+	    outputProps.sdi4kTransport == SDI4KTransport::Squares) {
+		if (outputProps.ioSelect == IOSelection::SDI1_2) {
+			outputProps.ioSelect = IOSelection::SDI1_2_Squares;
+		} else if (outputProps.ioSelect == IOSelection::SDI3_4) {
+			outputProps.ioSelect = IOSelection::SDI3_4_Squares;
+		}
+	}
+
+	const std::string &ioSelectStr =
+		aja::IOSelectionToString(outputProps.ioSelect);
+
+	NTV2OutputDestinations outputDests;
+	aja::IOSelectionToOutputDests(outputProps.ioSelect, outputDests);
+	if (outputDests.empty()) {
+		blog(LOG_ERROR,
+		     "No Output Destinations found for IOSelection %s!",
+		     ioSelectStr.c_str());
+		return nullptr;
+	}
+	outputProps.outputDest = *outputDests.begin();
+
+	if (!cardEntry->AcquireOutputSelection(outputProps.ioSelect, deviceID,
+					       outputID)) {
+		blog(LOG_ERROR,
+		     "aja_output_create: Error acquiring IOSelection %s for card ID %s",
+		     ioSelectStr.c_str(), cardID);
+		return nullptr;
+	}
+
+	auto ajaOutput = new AJAOutput(card, cardID, outputID,
+				       (UWord)cardEntry->GetCardIndex(),
+				       deviceID);
+
+	ajaOutput->Initialize(outputProps);
+	ajaOutput->ClearVideoQueue();
+	ajaOutput->ClearAudioQueue();
+	ajaOutput->SetOBSOutput(output);
+	ajaOutput->CreateThread(true);
+
+#ifdef AJA_WRITE_DEBUG_WAV
+	AJAWavWriterAudioFormat wavFormat;
+	wavFormat.channelCount = outputProps.AudioChannels();
+	wavFormat.sampleRate = outputProps.audioSampleRate;
+	wavFormat.sampleSize = outputProps.AudioSize();
+	ajaOutput->mWaveWriter =
+		new AJAWavWriter("obs_aja_output.wav", wavFormat);
+	ajaOutput->mWaveWriter->open();
+#endif
+
+	blog(LOG_INFO, "AJA Output created!");
+
+	return ajaOutput;
+}
+
+static void aja_output_update(void *data, obs_data_t *settings)
+{
+	UNUSED_PARAMETER(data);
+	UNUSED_PARAMETER(settings);
+	blog(LOG_INFO, "AJA Output Update...");
+}
+
+static bool aja_output_start(void *data)
+{
+	blog(LOG_INFO, "Starting AJA Output...");
+
+	auto ajaOutput = (AJAOutput *)data;
+	if (!ajaOutput) {
+		blog(LOG_ERROR, "aja_output_start: Plugin instance is null!");
+		return false;
+	}
+
+	const std::string &cardID = ajaOutput->mCardID;
+	auto &cardManager = aja::CardManager::Instance();
+
+	cardManager.EnumerateCards();
+
+	auto cardEntry = cardManager.GetCardEntry(cardID);
+	if (!cardEntry) {
+		blog(LOG_DEBUG,
+		     "aja_io_selection_changed: Card Entry not found for %s",
+		     cardID.c_str());
+		return false;
+	}
+
+	CNTV2Card *card = ajaOutput->GetCard();
+	if (!card) {
+		blog(LOG_ERROR, "aja_output_start: Card instance is null!");
+		return false;
+	}
+
+	auto outputProps = ajaOutput->GetOutputProps();
+	auto audioSystem = outputProps.AudioSystem();
+	auto outputDest = outputProps.outputDest;
+	auto videoFormat = outputProps.videoFormat;
+	auto pixelFormat = outputProps.pixelFormat;
+
+	blog(LOG_INFO,
+	     "Output Dest: %s | Audio System: %s | Video Format: %s | Pixel Format: %s",
+	     NTV2OutputDestinationToString(outputDest, true).c_str(),
+	     NTV2AudioSystemToString(audioSystem, true).c_str(),
+	     NTV2VideoFormatToString(videoFormat, false).c_str(),
+	     NTV2FrameBufferFormatToString(pixelFormat, true).c_str());
+
+	const NTV2DeviceID deviceID = card->GetDeviceID();
+
+	if (GetIndexForNTV2Channel(outputProps.Channel()) > 0) {
+		auto numFramestores = aja::CardNumFramestores(deviceID);
+		for (UWord i = 0; i < numFramestores; i++) {
+			auto channel = GetNTV2ChannelForIndex(i);
+			if (cardEntry->ChannelReady(channel,
+						    ajaOutput->mOutputID)) {
+				card->SetVideoFormat(videoFormat, false, false,
+						     channel);
+				card->SetRegisterWriteMode(
+					NTV2_REGWRITE_SYNCTOFRAME, channel);
+				card->SetFrameBufferFormat(channel,
+							   pixelFormat);
+			}
+		}
+	}
+
+	// Configures crosspoint routing on AJA card
+	if (!Routing::ConfigureOutputRoute(outputProps, NTV2_MODE_DISPLAY,
+					   card)) {
+		blog(LOG_ERROR,
+		     "aja_output_start: Error configuring output route!");
+		return false;
+	}
+
+	Routing::ConfigureOutputAudio(outputProps, card);
+
+	const auto &formatDesc = outputProps.FormatDesc();
+	struct video_scale_info scaler = {};
+	scaler.format = aja::AJAPixelFormatToOBSVideoFormat(pixelFormat);
+	scaler.width = formatDesc.GetRasterWidth();
+	scaler.height = formatDesc.GetRasterHeight();
+	// TODO(paulh): Find out what these scaler params actually do.
+	// The colors are off when outputting the frames that OBS sends us.
+	// but simply changing these values doesn't seem to have any effect.
+	scaler.colorspace = VIDEO_CS_709;
+	scaler.range = VIDEO_RANGE_PARTIAL;
+
+	obs_output_set_video_conversion(ajaOutput->GetOBSOutput(), &scaler);
+
+	struct audio_convert_info conversion = {};
+	conversion.format = outputProps.AudioFormat();
+	conversion.speakers = outputProps.SpeakerLayout();
+	conversion.samples_per_sec = outputProps.audioSampleRate;
+
+	obs_output_set_audio_conversion(ajaOutput->GetOBSOutput(), &conversion);
+
+	if (!obs_output_begin_data_capture(ajaOutput->GetOBSOutput(), 0)) {
+		blog(LOG_ERROR,
+		     "aja_output_start: Begin OBS data capture failed!");
+		return false;
+	}
+
+	blog(LOG_INFO, "AJA Output started!");
+
+	return true;
+}
+
+static void aja_output_stop(void *data, uint64_t ts)
+{
+	UNUSED_PARAMETER(ts);
+
+	blog(LOG_INFO, "Stopping AJA Output...");
+
+	auto ajaOutput = (AJAOutput *)data;
+	if (!ajaOutput) {
+		blog(LOG_ERROR, "aja_output_stop: Plugin instance is null!");
+		return;
+	}
+
+	auto outputProps = ajaOutput->GetOutputProps();
+	const std::string &cardID = ajaOutput->mCardID;
+	auto &cardManager = aja::CardManager::Instance();
+
+	cardManager.EnumerateCards();
+
+	auto cardEntry = cardManager.GetCardEntry(cardID);
+	if (!cardEntry) {
+		blog(LOG_ERROR, "aja_output_stop: Card Entry not found for %s",
+		     cardID.c_str());
+		return;
+	}
+
+	CNTV2Card *card = ajaOutput->GetCard();
+	if (!card) {
+		blog(LOG_ERROR, "aja_output_stop: Card instance is null!");
+		return;
+	}
+
+	if (!cardEntry->ReleaseOutputSelection(outputProps.ioSelect,
+					       card->GetDeviceID(),
+					       ajaOutput->mOutputID)) {
+		blog(LOG_WARNING,
+		     "aja_output_stop: Error releasing IOSelection %s from card ID %s",
+		     aja::IOSelectionToString(outputProps.ioSelect).c_str(),
+		     cardID.c_str());
+	}
+
+	auto audioSystem = outputProps.AudioSystem();
+
+	ajaOutput->GenerateTestPattern(outputProps.videoFormat,
+				       outputProps.pixelFormat,
+				       NTV2_TestPatt_Black);
+
+	obs_output_end_data_capture(ajaOutput->GetOBSOutput());
+
+	card->StopAudioOutput(audioSystem);
+
+	blog(LOG_INFO, "AJA Output stopped.");
+}
+
+static void aja_output_raw_video(void *data, struct video_data *frame)
+{
+	auto ajaOutput = (AJAOutput *)data;
+	if (!ajaOutput)
+		return;
+
+	auto outputProps = ajaOutput->GetOutputProps();
+	auto rasterBytes = outputProps.FormatDesc().GetTotalRasterBytes();
+	ajaOutput->QueueVideoFrame(frame, rasterBytes);
+}
+
+static void aja_output_raw_audio(void *data, struct audio_data *frames)
+{
+	auto ajaOutput = (AJAOutput *)data;
+	if (!ajaOutput)
+		return;
+
+	auto outputProps = ajaOutput->GetOutputProps();
+	auto audioSize = outputProps.AudioSize();
+	auto audioBytes = static_cast<ULWord>(frames->frames * audioSize);
+	ajaOutput->QueueAudioFrames(frames, audioBytes);
+}
+
+static obs_properties_t *aja_output_get_properties(void *data)
+{
+	obs_properties_t *props = obs_properties_create();
+
+	obs_property_t *device_list = obs_properties_add_list(
+		props, kUIPropDevice.id, obs_module_text(kUIPropDevice.text),
+		OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
+
+	obs_property_t *output_list = obs_properties_add_list(
+		props, kUIPropOutput.id, obs_module_text(kUIPropOutput.text),
+		OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+
+	obs_property_t *vid_fmt_list = obs_properties_add_list(
+		props, kUIPropVideoFormatSelect.id,
+		obs_module_text(kUIPropVideoFormatSelect.text),
+		OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+
+	obs_properties_add_list(props, kUIPropPixelFormatSelect.id,
+				obs_module_text(kUIPropPixelFormatSelect.text),
+				OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+
+	obs_properties_add_list(props, kUIPropSDI4KTransport.id,
+				obs_module_text(kUIPropSDI4KTransport.text),
+				OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+
+	obs_properties_add_bool(props, kUIPropAutoStartOutput.id,
+				obs_module_text(kUIPropAutoStartOutput.text));
+
+	obs_property_set_modified_callback(vid_fmt_list,
+					   aja_video_format_changed);
+	obs_property_set_modified_callback(output_list,
+					   aja_output_dest_changed);
+	obs_property_set_modified_callback2(device_list,
+					    aja_output_device_changed, data);
+
+	return props;
+}
+
+static const char *aja_output_get_name(void *)
+{
+	return obs_module_text(kUIPropOutputModule.text);
+}
+
+// NOTE(paulh): Drop-down defaults are set on a clean launch in aja-output-ui code.
+// Otherwise we load the settings stored in the ajaOutputProps/ajaPreviewOutputProps.json configs.
+void aja_output_get_defaults(obs_data_t *settings)
+{
+	obs_data_set_default_bool(settings, kUIPropAutoStartOutput.id, false);
+}
+
+struct obs_output_info create_aja_output_info()
+{
+	struct obs_output_info aja_output_info = {};
+
+	aja_output_info.id = kUIPropOutputModule.id;
+	aja_output_info.flags = OBS_OUTPUT_AV;
+	aja_output_info.get_name = aja_output_get_name;
+	aja_output_info.create = aja_output_create;
+	aja_output_info.destroy = aja_output_destroy;
+	aja_output_info.start = aja_output_start;
+	aja_output_info.stop = aja_output_stop;
+	aja_output_info.raw_video = aja_output_raw_video;
+	aja_output_info.raw_audio = aja_output_raw_audio;
+	aja_output_info.update = aja_output_update;
+	aja_output_info.get_defaults = aja_output_get_defaults;
+	aja_output_info.get_properties = aja_output_get_properties;
+
+	return aja_output_info;
+}

+ 160 - 0
plugins/aja/aja-output.hpp

@@ -0,0 +1,160 @@
+#pragma once
+
+#include "aja-props.hpp"
+
+#include <ajantv2/includes/ntv2testpatterngen.h>
+
+#include <ajabase/common/types.h>
+#include <ajabase/system/thread.h>
+
+// #define AJA_WRITE_DEBUG_WAV
+#ifdef AJA_WRITE_DEBUG_WAV
+#include <ajabase/common/wavewriter.h>
+#endif
+
+#include <deque>
+#include <memory>
+#include <mutex>
+
+struct VideoFrame {
+	struct video_data frame;
+	int64_t frameNum;
+	size_t size;
+};
+
+struct AudioFrames {
+	struct audio_data frames;
+	size_t offset;
+	size_t size;
+};
+
+//TODO(paulh): Refactor me into OutputProps
+struct FrameTimes {
+	double obsFps;
+	uint64_t obsFrameTime;
+	double cardFps;
+	uint64_t cardFrameTime;
+};
+
+using VideoQueue = std::deque<VideoFrame>;
+using AudioQueue = std::deque<AudioFrames>;
+
+class CNTV2Card; // forward decl
+
+class AJAOutput {
+public:
+	enum {
+		// min queue sizes computed in AJAOutput
+		kVideoQueueMaxSize = 15,
+		kAudioQueueMaxSize =
+			96, // ~(48000 / 1024 samples per audio_frame) * 2sec
+	};
+
+	AJAOutput(CNTV2Card *card, const std::string &cardID,
+		  const std::string &outputID, UWord deviceIndex,
+		  const NTV2DeviceID deviceID);
+
+	~AJAOutput();
+
+	CNTV2Card *GetCard();
+
+	void Initialize(const OutputProps &props);
+
+	void SetOBSOutput(obs_output_t *output);
+	obs_output_t *GetOBSOutput();
+
+	void SetOutputProps(const OutputProps &props);
+	OutputProps GetOutputProps() const;
+
+	void GenerateTestPattern(NTV2VideoFormat vf, NTV2PixelFormat pf,
+				 NTV2TestPatternSelect pattern);
+
+	void QueueVideoFrame(struct video_data *frame, size_t size);
+	void QueueAudioFrames(struct audio_data *frames, size_t size);
+	void ClearVideoQueue();
+	void ClearAudioQueue();
+	size_t VideoQueueSize();
+	size_t AudioQueueSize();
+
+	bool HaveEnoughAudio(size_t needAudioSize);
+	void DMAAudioFromQueue(NTV2AudioSystem audioSys);
+	void DMAVideoFromQueue();
+
+	void CreateThread(bool enable = false);
+	void StopThread();
+	bool ThreadRunning();
+	static void OutputThread(AJAThread *thread, void *ctx);
+
+	std::string mCardID;
+	std::string mOutputID;
+	UWord mDeviceIndex;
+	NTV2DeviceID mDeviceID;
+
+	FrameTimes mFrameTimes;
+
+	uint32_t mAudioPlayCursor;
+	uint32_t mAudioWriteCursor;
+	uint32_t mAudioWrapAddress;
+	uint32_t mAudioRate;
+
+	uint64_t mAudioQueueSamples;
+	uint64_t mAudioWriteSamples;
+	uint64_t mAudioPlaySamples;
+
+	uint32_t mNumCardFrames;
+	uint32_t mFirstCardFrame;
+	uint32_t mLastCardFrame;
+	uint32_t mWriteCardFrame;
+	uint32_t mPlayCardFrame;
+	uint32_t mPlayCardNext;
+	uint32_t mFrameRateNum;
+	uint32_t mFrameRateDen;
+
+	uint64_t mVideoQueueFrames;
+	uint64_t mVideoWriteFrames;
+	uint64_t mVideoPlayFrames;
+
+	uint64_t mFirstVideoTS;
+	uint64_t mFirstAudioTS;
+	uint64_t mLastVideoTS;
+	uint64_t mLastAudioTS;
+
+	int64_t mVideoDelay;
+	int64_t mAudioDelay;
+	int64_t mAudioVideoSync;
+	int64_t mAudioAdjust;
+	int64_t mLastStatTime;
+#ifdef AJA_WRITE_DEBUG_WAV
+	AJAWavWriter *mWaveWriter;
+#endif
+
+private:
+	void calculate_card_frame_indices(uint32_t numFrames, NTV2DeviceID id,
+					  NTV2Channel channel,
+					  NTV2VideoFormat vf,
+					  NTV2PixelFormat pf);
+
+	uint32_t get_frame_count();
+
+	void dma_audio_samples(NTV2AudioSystem audioSys, uint32_t *data,
+			       size_t size);
+
+	CNTV2Card *mCard;
+
+	OutputProps mOutputProps;
+
+	NTV2TestPatternBuffer mTestPattern;
+
+	bool mIsRunning;
+	bool mAudioStarted;
+
+	AJAThread mRunThread;
+	mutable std::mutex mVideoLock;
+	mutable std::mutex mAudioLock;
+	mutable std::mutex mRunThreadLock;
+
+	std::unique_ptr<VideoQueue> mVideoQueue;
+	std::unique_ptr<AudioQueue> mAudioQueue;
+
+	obs_output_t *mOBSOutput;
+};

+ 436 - 0
plugins/aja/aja-props.cpp

@@ -0,0 +1,436 @@
+#include "aja-props.hpp"
+
+#include <ajantv2/includes/ntv2devicefeatures.h>
+#include <ajantv2/includes/ntv2utils.h>
+#include <ajantv2/includes/ntv2vpid.h>
+
+VPIDData::VPIDData()
+	: mVpidA{0},
+	  mVpidB{0},
+	  mStandardA{VPIDStandard_Unknown},
+	  mStandardB{VPIDStandard_Unknown},
+	  mSamplingA{VPIDSampling_XYZ_444},
+	  mSamplingB{VPIDSampling_XYZ_444}
+{
+}
+
+VPIDData::VPIDData(ULWord vpidA, ULWord vpidB)
+	: mVpidA{vpidA},
+	  mVpidB{vpidB},
+	  mStandardA{VPIDStandard_Unknown},
+	  mStandardB{VPIDStandard_Unknown},
+	  mSamplingA{VPIDSampling_XYZ_444},
+	  mSamplingB{VPIDSampling_XYZ_444}
+{
+	Parse();
+}
+
+VPIDData::VPIDData(const VPIDData &other)
+	: mVpidA{other.mVpidA},
+	  mVpidB{other.mVpidB},
+	  mStandardA{VPIDStandard_Unknown},
+	  mStandardB{VPIDStandard_Unknown},
+	  mSamplingA{VPIDSampling_XYZ_444},
+	  mSamplingB{VPIDSampling_XYZ_444}
+{
+	Parse();
+}
+VPIDData::VPIDData(VPIDData &&other)
+	: mVpidA{other.mVpidA},
+	  mVpidB{other.mVpidB},
+	  mStandardA{VPIDStandard_Unknown},
+	  mStandardB{VPIDStandard_Unknown},
+	  mSamplingA{VPIDSampling_XYZ_444},
+	  mSamplingB{VPIDSampling_XYZ_444}
+{
+	Parse();
+}
+
+VPIDData &VPIDData::operator=(const VPIDData &other)
+{
+	mVpidA = other.mVpidA;
+	mVpidB = other.mVpidB;
+	return *this;
+}
+
+VPIDData &VPIDData::operator=(VPIDData &&other)
+{
+	mVpidA = other.mVpidA;
+	mVpidB = other.mVpidB;
+	return *this;
+}
+
+bool VPIDData::operator==(const VPIDData &rhs) const
+{
+	return (mVpidA == rhs.mVpidA && mVpidB == rhs.mVpidB);
+}
+
+bool VPIDData::operator!=(const VPIDData &rhs) const
+{
+	return !operator==(rhs);
+}
+
+void VPIDData::SetA(ULWord vpidA)
+{
+	mVpidA = vpidA;
+}
+
+void VPIDData::SetB(ULWord vpidB)
+{
+	mVpidB = vpidB;
+}
+
+void VPIDData::Parse()
+{
+	CNTV2VPID parserA;
+	parserA.SetVPID(mVpidA);
+	mStandardA = parserA.GetStandard();
+	mSamplingA = parserA.GetSampling();
+
+	CNTV2VPID parserB;
+	parserB.SetVPID(mVpidB);
+	mStandardB = parserB.GetStandard();
+	mSamplingB = parserB.GetSampling();
+}
+
+bool VPIDData::IsRGB() const
+{
+	switch (mSamplingA) {
+	default:
+		break;
+	case VPIDSampling_GBR_444:
+	case VPIDSampling_GBRA_4444:
+	case VPIDSampling_GBRD_4444:
+		return true;
+	}
+	return false;
+}
+
+VPIDStandard VPIDData::Standard() const
+{
+	return mStandardA;
+}
+
+VPIDSampling VPIDData::Sampling() const
+{
+	return mSamplingA;
+}
+
+// AJASource Properties
+SourceProps::SourceProps()
+	: deviceID{DEVICE_ID_NOTFOUND},
+	  ioSelect{IOSelection::Invalid},
+	  inputSource{NTV2_INPUTSOURCE_INVALID},
+	  videoFormat{NTV2_FORMAT_UNKNOWN},
+	  pixelFormat{NTV2_FBF_INVALID},
+	  sdi4kTransport{SDI4KTransport::TwoSampleInterleave},
+	  audioNumChannels{8},
+	  audioSampleSize{4},
+	  audioSampleRate{48000},
+	  vpids{},
+	  autoDetect{false},
+	  deactivateWhileNotShowing{false}
+{
+}
+
+SourceProps::SourceProps(NTV2DeviceID devID)
+	: deviceID{devID},
+	  ioSelect{IOSelection::Invalid},
+	  inputSource{NTV2_INPUTSOURCE_INVALID},
+	  videoFormat{NTV2_FORMAT_UNKNOWN},
+	  pixelFormat{NTV2_FBF_INVALID},
+	  sdi4kTransport{SDI4KTransport::TwoSampleInterleave},
+	  audioNumChannels{8},
+	  audioSampleSize{4},
+	  audioSampleRate{48000},
+	  vpids{},
+	  autoDetect{false},
+	  deactivateWhileNotShowing{false}
+{
+}
+
+SourceProps::SourceProps(const SourceProps &props)
+{
+	deviceID = props.deviceID;
+	ioSelect = props.ioSelect;
+	inputSource = props.inputSource;
+	videoFormat = props.videoFormat;
+	pixelFormat = props.pixelFormat;
+	sdi4kTransport = props.sdi4kTransport;
+	audioNumChannels = props.audioNumChannels;
+	audioSampleSize = props.audioSampleSize;
+	audioSampleRate = props.audioSampleRate;
+	vpids = props.vpids;
+	autoDetect = props.autoDetect;
+	deactivateWhileNotShowing = props.deactivateWhileNotShowing;
+}
+
+SourceProps::SourceProps(SourceProps &&props)
+{
+	deviceID = props.deviceID;
+	ioSelect = props.ioSelect;
+	inputSource = props.inputSource;
+	videoFormat = props.videoFormat;
+	pixelFormat = props.pixelFormat;
+	sdi4kTransport = props.sdi4kTransport;
+	audioNumChannels = props.audioNumChannels;
+	audioSampleSize = props.audioSampleSize;
+	audioSampleRate = props.audioSampleRate;
+	vpids = props.vpids;
+	autoDetect = props.autoDetect;
+	deactivateWhileNotShowing = props.deactivateWhileNotShowing;
+}
+
+void SourceProps::operator=(const SourceProps &props)
+{
+	deviceID = props.deviceID;
+	ioSelect = props.ioSelect;
+	inputSource = props.inputSource;
+	videoFormat = props.videoFormat;
+	pixelFormat = props.pixelFormat;
+	sdi4kTransport = props.sdi4kTransport;
+	audioNumChannels = props.audioNumChannels;
+	audioSampleSize = props.audioSampleSize;
+	audioSampleRate = props.audioSampleRate;
+	vpids = props.vpids;
+	autoDetect = props.autoDetect;
+	deactivateWhileNotShowing = props.deactivateWhileNotShowing;
+}
+
+void SourceProps::operator=(SourceProps &&props)
+{
+	deviceID = props.deviceID;
+	ioSelect = props.ioSelect;
+	inputSource = props.inputSource;
+	videoFormat = props.videoFormat;
+	pixelFormat = props.pixelFormat;
+	sdi4kTransport = props.sdi4kTransport;
+	audioNumChannels = props.audioNumChannels;
+	audioSampleSize = props.audioSampleSize;
+	audioSampleRate = props.audioSampleRate;
+	vpids = props.vpids;
+	autoDetect = props.autoDetect;
+	deactivateWhileNotShowing = props.deactivateWhileNotShowing;
+}
+
+bool SourceProps::operator==(const SourceProps &props)
+{
+	return (deviceID == props.deviceID && ioSelect == props.ioSelect &&
+		// inputSource == props.inputSource &&
+		videoFormat == props.videoFormat &&
+		pixelFormat == props.pixelFormat &&
+		// vpid == props.vpid &&
+		autoDetect == props.autoDetect &&
+		sdi4kTransport == props.sdi4kTransport &&
+		audioNumChannels == props.audioNumChannels &&
+		audioSampleSize == props.audioSampleSize &&
+		audioSampleRate == props.audioSampleRate &&
+		deactivateWhileNotShowing == props.deactivateWhileNotShowing);
+}
+
+bool SourceProps::operator!=(const SourceProps &props)
+{
+	return !operator==(props);
+}
+
+NTV2Channel SourceProps::Channel() const
+{
+	return NTV2InputSourceToChannel(inputSource);
+}
+
+NTV2AudioSystem SourceProps::AudioSystem() const
+{
+	return NTV2ChannelToAudioSystem(Channel());
+}
+
+NTV2AudioRate SourceProps::AudioRate() const
+{
+	NTV2AudioRate rate = NTV2_AUDIO_48K;
+	switch (audioSampleRate) {
+	default:
+	case 48000:
+		rate = NTV2_AUDIO_48K;
+		break;
+	case 96000:
+		rate = NTV2_AUDIO_96K;
+		break;
+	case 192000:
+		rate = NTV2_AUDIO_192K;
+		break;
+	}
+
+	return rate;
+}
+
+// Size in bytes of N channels of audio
+size_t SourceProps::AudioSize() const
+{
+	return audioNumChannels * audioSampleSize;
+}
+
+audio_format SourceProps::AudioFormat() const
+{
+	// NTV2 is always 32-bit PCM
+	return AUDIO_FORMAT_32BIT;
+}
+
+speaker_layout SourceProps::SpeakerLayout() const
+{
+	if (audioNumChannels == 2)
+		return SPEAKERS_STEREO;
+	// NTV2 is always at least 8ch on modern boards
+	return SPEAKERS_7POINT1;
+}
+
+//
+// AJAOutput Properties
+//
+OutputProps::OutputProps(NTV2DeviceID devID)
+	: deviceID{devID},
+	  ioSelect{IOSelection::Invalid},
+	  outputDest{NTV2_OUTPUTDESTINATION_ANALOG},
+	  videoFormat{NTV2_FORMAT_UNKNOWN},
+	  pixelFormat{NTV2_FBF_INVALID},
+	  sdi4kTransport{SDI4KTransport::TwoSampleInterleave},
+	  audioNumChannels{8},
+	  audioSampleSize{4},
+	  audioSampleRate{48000}
+{
+}
+
+OutputProps::OutputProps(OutputProps &&props)
+{
+	deviceID = props.deviceID;
+	ioSelect = props.ioSelect;
+	outputDest = props.outputDest;
+	videoFormat = props.videoFormat;
+	pixelFormat = props.pixelFormat;
+	sdi4kTransport = props.sdi4kTransport;
+	audioNumChannels = props.audioNumChannels;
+	audioSampleSize = props.audioSampleSize;
+	audioSampleRate = props.audioSampleRate;
+}
+
+OutputProps::OutputProps(const OutputProps &props)
+{
+	deviceID = props.deviceID;
+	ioSelect = props.ioSelect;
+	outputDest = props.outputDest;
+	videoFormat = props.videoFormat;
+	pixelFormat = props.pixelFormat;
+	sdi4kTransport = props.sdi4kTransport;
+	audioNumChannels = props.audioNumChannels;
+	audioSampleSize = props.audioSampleSize;
+	audioSampleRate = props.audioSampleRate;
+}
+
+void OutputProps::operator=(const OutputProps &props)
+{
+	deviceID = props.deviceID;
+	ioSelect = props.ioSelect;
+	outputDest = props.outputDest;
+	videoFormat = props.videoFormat;
+	pixelFormat = props.pixelFormat;
+	sdi4kTransport = props.sdi4kTransport;
+	audioNumChannels = props.audioNumChannels;
+	audioSampleSize = props.audioSampleSize;
+	audioSampleRate = props.audioSampleRate;
+}
+
+void OutputProps::operator=(OutputProps &&props)
+{
+	deviceID = props.deviceID;
+	ioSelect = props.ioSelect;
+	outputDest = props.outputDest;
+	videoFormat = props.videoFormat;
+	pixelFormat = props.pixelFormat;
+	sdi4kTransport = props.sdi4kTransport;
+	audioNumChannels = props.audioNumChannels;
+	audioSampleSize = props.audioSampleSize;
+	audioSampleRate = props.audioSampleRate;
+}
+
+bool OutputProps::operator==(const OutputProps &props)
+{
+	return (deviceID == props.deviceID && ioSelect == props.ioSelect &&
+		// outputDest == props.outputDest &&
+		videoFormat == props.videoFormat &&
+		pixelFormat == props.pixelFormat &&
+		audioNumChannels == props.audioNumChannels &&
+		audioSampleSize == props.audioSampleSize &&
+		audioSampleRate == props.audioSampleRate);
+}
+
+bool OutputProps::operator!=(const OutputProps &props)
+{
+	return !operator==(props);
+}
+
+NTV2FormatDesc OutputProps::FormatDesc()
+{
+	return NTV2FormatDesc(videoFormat, pixelFormat, NTV2_VANCMODE_OFF);
+}
+
+NTV2Channel OutputProps::Channel() const
+{
+	// Output Channel Special Cases
+	// KONA1 -- Has 2 framestores but only 1 bi-directional SDI widget
+	if (deviceID == DEVICE_ID_KONA1) {
+		return NTV2_CHANNEL2;
+	} else if ((deviceID == DEVICE_ID_IO4K ||
+		    deviceID == DEVICE_ID_IO4KPLUS) &&
+		   outputDest == NTV2_OUTPUTDESTINATION_SDI5) {
+		// IO4K/IO4K+ SDI Monitor - Use framestore 4 but SDI5
+		return NTV2_CHANNEL4;
+	}
+
+	if (NTV2_OUTPUT_DEST_IS_HDMI(outputDest))
+		return static_cast<NTV2Channel>(
+			NTV2DeviceGetNumFrameStores(deviceID) - 1);
+
+	return NTV2OutputDestinationToChannel(outputDest);
+}
+
+NTV2AudioSystem OutputProps::AudioSystem() const
+{
+	return NTV2ChannelToAudioSystem(Channel());
+}
+
+NTV2AudioRate OutputProps::AudioRate() const
+{
+	NTV2AudioRate rate = NTV2_AUDIO_48K;
+	switch (audioSampleRate) {
+	default:
+	case 48000:
+		rate = NTV2_AUDIO_48K;
+		break;
+	case 96000:
+		rate = NTV2_AUDIO_96K;
+		break;
+	case 192000:
+		rate = NTV2_AUDIO_192K;
+		break;
+	}
+
+	return rate;
+}
+
+// Size in bytes of N channels of audio
+size_t OutputProps::AudioSize() const
+{
+	return audioNumChannels * audioSampleSize;
+}
+
+audio_format OutputProps::AudioFormat() const
+{
+	// NTV2 is always 32-bit PCM
+	return AUDIO_FORMAT_32BIT;
+}
+
+speaker_layout OutputProps::SpeakerLayout() const
+{
+	if (audioNumChannels == 2)
+		return SPEAKERS_STEREO;
+	// NTV2 is always at least 8ch on modern boards
+	return SPEAKERS_7POINT1;
+}

+ 108 - 0
plugins/aja/aja-props.hpp

@@ -0,0 +1,108 @@
+#pragma once
+
+#include "aja-enums.hpp"
+
+#include <media-io/audio-io.h>
+
+#include <ajantv2/includes/ntv2enums.h>
+#include <ajantv2/includes/ntv2formatdescriptor.h>
+
+#include <map>
+#include <string>
+#include <vector>
+
+class VPIDData {
+public:
+	VPIDData();
+	VPIDData(ULWord vpidA, ULWord vpidB);
+	VPIDData(const VPIDData &other);
+	VPIDData(VPIDData &&other);
+	~VPIDData() = default;
+
+	VPIDData &operator=(const VPIDData &other);
+	VPIDData &operator=(VPIDData &&other);
+	bool operator==(const VPIDData &rhs) const;
+	bool operator!=(const VPIDData &rhs) const;
+
+	void SetA(ULWord vpidA);
+	void SetB(ULWord vpidB);
+	void Parse();
+	bool IsRGB() const;
+
+	VPIDStandard Standard() const;
+	VPIDSampling Sampling() const;
+
+private:
+	ULWord mVpidA;
+	ULWord mVpidB;
+	VPIDStandard mStandardA;
+	VPIDSampling mSamplingA;
+	VPIDStandard mStandardB;
+	VPIDSampling mSamplingB;
+};
+
+using VPIDDataList = std::vector<VPIDData>;
+
+//TODO(paulh): Consolidate the two Props classes
+class SourceProps {
+public:
+	explicit SourceProps();
+	explicit SourceProps(NTV2DeviceID devID);
+	~SourceProps() = default;
+	SourceProps(const SourceProps &props);
+	SourceProps(SourceProps &&props);
+	void operator=(const SourceProps &props);
+	void operator=(SourceProps &&props);
+	bool operator==(const SourceProps &props);
+	bool operator!=(const SourceProps &props);
+
+	NTV2Channel Channel() const;
+	NTV2AudioSystem AudioSystem() const;
+	NTV2AudioRate AudioRate() const;
+	size_t AudioSize() const;
+	audio_format AudioFormat() const;
+	speaker_layout SpeakerLayout() const;
+
+	NTV2DeviceID deviceID;
+	IOSelection ioSelect;
+	NTV2InputSource inputSource;
+	NTV2VideoFormat videoFormat;
+	NTV2PixelFormat pixelFormat;
+	SDI4KTransport sdi4kTransport;
+	VPIDDataList vpids;
+	uint32_t audioNumChannels;
+	uint32_t audioSampleSize;
+	uint32_t audioSampleRate;
+	bool autoDetect;
+	bool deactivateWhileNotShowing;
+};
+
+class OutputProps {
+public:
+	explicit OutputProps(NTV2DeviceID devID);
+	~OutputProps() = default;
+	OutputProps(const OutputProps &props);
+	OutputProps(OutputProps &&props);
+	void operator=(const OutputProps &props);
+	void operator=(OutputProps &&props);
+	bool operator==(const OutputProps &props);
+	bool operator!=(const OutputProps &props);
+
+	NTV2FormatDesc FormatDesc();
+	NTV2Channel Channel() const;
+	NTV2AudioSystem AudioSystem() const;
+	NTV2AudioRate AudioRate() const;
+	size_t AudioSize() const;
+	audio_format AudioFormat() const;
+	speaker_layout SpeakerLayout() const;
+
+	NTV2DeviceID deviceID;
+	IOSelection ioSelect;
+	NTV2OutputDestination outputDest;
+	NTV2VideoFormat videoFormat;
+	NTV2PixelFormat pixelFormat;
+	SDI4KTransport sdi4kTransport;
+	uint32_t audioNumChannels;
+	uint32_t audioSampleSize;
+	uint32_t audioSampleRate;
+};

+ 823 - 0
plugins/aja/aja-routing.cpp

@@ -0,0 +1,823 @@
+#include "aja-card-manager.hpp"
+#include "aja-common.hpp"
+#include "aja-routing.hpp"
+#include "aja-widget-io.hpp"
+
+// Signal routing crosspoint and register setting tables for SDI/HDMI/etc.
+#include "routing/hdmi_rgb_capture.h"
+#include "routing/hdmi_rgb_display.h"
+#include "routing/hdmi_ycbcr_capture.h"
+#include "routing/hdmi_ycbcr_display.h"
+#include "routing/sdi_ycbcr_capture.h"
+#include "routing/sdi_ycbcr_display.h"
+#include "routing/sdi_rgb_capture.h"
+#include "routing/sdi_rgb_display.h"
+
+#include <ajabase/common/common.h>
+#include <ajantv2/includes/ntv2card.h>
+#include <ajantv2/includes/ntv2devicefeatures.h>
+
+#include <obs-module.h>
+
+RasterDefinition GetRasterDefinition(IOSelection io, NTV2VideoFormat vf,
+				     NTV2DeviceID deviceID)
+{
+	RasterDefinition def = RasterDefinition::Unknown;
+
+	if (NTV2_IS_SD_VIDEO_FORMAT(vf)) {
+		def = RasterDefinition::SD;
+	} else if (NTV2_IS_HD_VIDEO_FORMAT(vf)) {
+		def = RasterDefinition::HD;
+	} else if (NTV2_IS_QUAD_FRAME_FORMAT(vf)) {
+		def = RasterDefinition::UHD_4K;
+
+		/* NOTE(paulh): Special enum for Kona5 Retail & IO4K+ firmwares which route UHD/4K formats
+		 * over 1x 6G/12G SDI using an undocumented crosspoint config.
+		 */
+		if (aja::IsSDIOneWireIOSelection(io) &&
+		    aja::IsRetailSDI12G(deviceID))
+			def = RasterDefinition::UHD_4K_Retail_12G;
+	} else if (NTV2_IS_QUAD_QUAD_FORMAT(vf)) {
+		def = RasterDefinition::UHD2_8K;
+	} else {
+		def = RasterDefinition::Unknown;
+	}
+
+	return def;
+}
+
+#define NTV2UTILS_ENUM_CASE_RETURN_STR(enum_name) \
+	case (enum_name):                         \
+		return #enum_name
+std::string RasterDefinitionToString(RasterDefinition rd)
+{
+	std::string str = "";
+
+	switch (rd) {
+		NTV2UTILS_ENUM_CASE_RETURN_STR(RasterDefinition::SD);
+		NTV2UTILS_ENUM_CASE_RETURN_STR(RasterDefinition::HD);
+		NTV2UTILS_ENUM_CASE_RETURN_STR(RasterDefinition::UHD_4K);
+		NTV2UTILS_ENUM_CASE_RETURN_STR(RasterDefinition::UHD2_8K);
+		NTV2UTILS_ENUM_CASE_RETURN_STR(RasterDefinition::Unknown);
+	}
+
+	return str;
+}
+
+/*
+ * Parse the widget routing shorthand string into a map of input and output NTV2CrosspointIDs.
+ * For example "sdi[0][0]->fb[0][0]" is shorthand for connecting the output crosspoint for
+ * SDI1/Datastream1 (NTV2_XptSDIIn1) to the input crosspoint for Framestore1/Datastream1 (NTV2_XptFrameBuffer1Input).
+ * These routing shorthand strings are found within the RoutingConfig structs in the "routing" sub-directory of the plugin.
+ */
+bool Routing::ParseRouteString(const std::string &route,
+			       NTV2XptConnections &cnx)
+{
+	blog(LOG_DEBUG, "aja::Routing::ParseRouteString: Input string: %s",
+	     route.c_str());
+
+	std::string route_lower(route);
+	route_lower = aja::lower(route_lower);
+	const std::string &route_strip = aja::replace(route_lower, " ", "");
+
+	if (route_strip.empty()) {
+		blog(LOG_DEBUG,
+		     "Routing::ParseRouteString: input string is empty!");
+		return false;
+	}
+
+	/* TODO(paulh): Tally up the lines and tokens and check that they are all parsed OK.
+	 * Right now we just return true if ANY tokens were parsed. This is OK _for now_ because
+	 * the route strings currently only come from a known set.
+	 */
+	NTV2StringList lines;
+	NTV2StringList tokens;
+
+	lines = aja::split(route_strip, ';');
+	if (lines.empty())
+		lines.push_back(route_strip);
+
+	int32_t parse_ok = 0;
+	for (const auto &l : lines) {
+		if (l.empty()) {
+			blog(LOG_DEBUG,
+			     "aja::Routing::ParseRouteString: Empty line!");
+			continue;
+		}
+
+		blog(LOG_DEBUG, "aja::Routing::ParseRouteString: Line: %s",
+		     l.c_str());
+
+		NTV2StringList tokens = aja::split(l, "->");
+		if (tokens.empty() || tokens.size() != 2) {
+			blog(LOG_DEBUG,
+			     "aja::Routing::ParseRouteString: Invalid token count!");
+			continue;
+		}
+
+		const std::string &left = tokens[0];  // output crosspoint
+		const std::string &right = tokens[1]; // input crosspoint
+		if (left.empty() || left.length() > 64) {
+			blog(LOG_DEBUG,
+			     "aja::Routing::ParseRouteString: Invalid Left token!");
+			continue;
+		}
+		if (right.empty() || right.length() > 64) {
+			blog(LOG_DEBUG,
+			     "aja::Routing::ParseRouteString: Invalid right token!");
+			continue;
+		}
+
+		blog(LOG_DEBUG,
+		     "aja::Routing::ParseRouteString: Left Token: %s -> Right Token: %s",
+		     left.c_str(), right.c_str());
+
+		// Parse Output Crosspoint from left token
+		int32_t out_chan = 0;
+		int32_t out_ds = 0;
+		std::string out_name(64, ' ');
+		if (std::sscanf(left.c_str(), "%[A-Za-z_0-9][%d][%d]",
+				&out_name[0], &out_chan, &out_ds)) {
+			out_name = aja::rstrip(out_name).substr(
+				0, out_name.find_first_of('\0'));
+
+			WidgetOutputSocket widget_out;
+			if (WidgetOutputSocket::Find(out_name,
+						     (NTV2Channel)out_chan,
+						     out_ds, widget_out)) {
+				blog(LOG_DEBUG,
+				     "aja::Routing::ParseRouteString: Found NTV2OutputCrosspointID %s",
+				     NTV2OutputCrosspointIDToString(
+					     widget_out.id)
+					     .c_str());
+
+				// Parse Input Crosspoint from right token
+				int32_t inp_chan = 0;
+				int32_t inp_ds = 0;
+				std::string inp_name(64, ' ');
+				if (std::sscanf(right.c_str(),
+						"%[A-Za-z_0-9][%d][%d]",
+						&inp_name[0], &inp_chan,
+						&inp_ds)) {
+					inp_name = aja::rstrip(inp_name).substr(
+						0,
+						inp_name.find_first_of('\0'));
+
+					WidgetInputSocket widget_inp;
+					if (WidgetInputSocket::Find(
+						    inp_name,
+						    (NTV2Channel)inp_chan,
+						    inp_ds, widget_inp)) {
+						blog(LOG_DEBUG,
+						     "aja::Routing::ParseRouteString: Found NTV2InputCrosspointID %s",
+						     NTV2InputCrosspointIDToString(
+							     widget_inp.id)
+							     .c_str());
+
+						cnx[widget_inp.id] =
+							widget_out.id;
+						parse_ok++;
+					} else {
+						blog(LOG_DEBUG,
+						     "aja::Routing::ParseRouteString: NTV2InputCrosspointID not found!");
+					}
+				}
+			} else {
+				blog(LOG_DEBUG,
+				     "aja::Routing::ParseRouteString: NTV2OutputCrosspointID not found!");
+			}
+		}
+	}
+
+	return parse_ok > 0;
+}
+
+// Determine the appropriate SDIWireFormat based on the specified device ID and VPID specification.
+bool Routing::DetermineSDIWireFormat(NTV2DeviceID deviceID, VPIDSpec spec,
+				     SDIWireFormat &swf)
+{
+	if (deviceID == DEVICE_ID_KONA5 || deviceID == DEVICE_ID_IO4KPLUS) {
+		static const std::vector<VPIDStandard> kRetail6GVpidStandards = {
+			VPIDStandard_2160_Single_6Gb,
+			VPIDStandard_1080_Single_6Gb,
+			VPIDStandard_1080_AFR_Single_6Gb,
+		};
+		static const std::vector<VPIDStandard> kRetail12GVpidStandards =
+			{VPIDStandard_2160_Single_12Gb,
+			 VPIDStandard_1080_10_12_AFR_Single_12Gb};
+		if (spec.first == RasterDefinition::UHD_4K &&
+		    aja::vec_contains<VPIDStandard>(kRetail6GVpidStandards,
+						    spec.second)) {
+			swf = SDIWireFormat::
+				UHD4K_ST2018_6G_Squares_2SI_Kona5_io4KPlus;
+			return true;
+		} else if (spec.first == RasterDefinition::UHD_4K &&
+			   aja::vec_contains<VPIDStandard>(
+				   kRetail12GVpidStandards, spec.second)) {
+			swf = SDIWireFormat::
+				UHD4K_ST2018_12G_Squares_2SI_Kona5_io4KPlus;
+			return true;
+		} else {
+			if (kSDIWireFormats.find(spec) !=
+			    kSDIWireFormats.end()) {
+				swf = kSDIWireFormats.at(spec);
+				return true;
+			}
+		}
+	} else {
+		if (kSDIWireFormats.find(spec) != kSDIWireFormats.end()) {
+			swf = kSDIWireFormats.at(spec);
+			return true;
+		}
+	}
+
+	return false;
+}
+
+// Lookup configuration for HDMI input/output in the routing table.
+bool Routing::FindRoutingConfigHDMI(HDMIWireFormat hwf, NTV2Mode mode,
+				    bool isRGB, NTV2DeviceID deviceID,
+				    RoutingConfig &routing)
+{
+	if (isRGB) {
+		if (mode == NTV2_MODE_CAPTURE) {
+			if (kHDMIRGBCaptureConfigs.find(hwf) !=
+			    kHDMIRGBCaptureConfigs.end()) {
+				routing = kHDMIRGBCaptureConfigs.at(hwf);
+				return true;
+			}
+		} else {
+			if (deviceID == DEVICE_ID_TTAP_PRO) {
+				routing = kHDMIRGBDisplayConfigs.at(
+					HDMIWireFormat::TTAP_PRO);
+				return true;
+			}
+			if (kHDMIRGBDisplayConfigs.find(hwf) !=
+			    kHDMIRGBDisplayConfigs.end()) {
+				routing = kHDMIRGBDisplayConfigs.at(hwf);
+				return true;
+			}
+		}
+	} else {
+		if (mode == NTV2_MODE_CAPTURE) {
+			if (kHDMIYCbCrCaptureConfigs.find(hwf) !=
+			    kHDMIYCbCrCaptureConfigs.end()) {
+				routing = kHDMIYCbCrCaptureConfigs.at(hwf);
+				return true;
+			}
+		} else {
+			if (kHDMIYCbCrDisplayConfigs.find(hwf) !=
+			    kHDMIYCbCrDisplayConfigs.end()) {
+				routing = kHDMIYCbCrDisplayConfigs.at(hwf);
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+// Lookup configuration for SDI input/output in the routing table.
+bool Routing::FindRoutingConfigSDI(SDIWireFormat swf, NTV2Mode mode, bool isRGB,
+				   NTV2DeviceID deviceID,
+				   RoutingConfig &routing)
+{
+	UNUSED_PARAMETER(deviceID);
+
+	if (isRGB) {
+		if (mode == NTV2_MODE_CAPTURE) {
+			if (kSDIRGBCaptureConfigs.find(swf) !=
+			    kSDIRGBCaptureConfigs.end()) {
+				routing = kSDIRGBCaptureConfigs.at(swf);
+				return true;
+			}
+		} else if (mode == NTV2_MODE_DISPLAY) {
+			if (kSDIRGBDisplayConfigs.find(swf) !=
+			    kSDIRGBDisplayConfigs.end()) {
+				routing = kSDIRGBDisplayConfigs.at(swf);
+				return true;
+			}
+		}
+	} else {
+		if (mode == NTV2_MODE_CAPTURE) {
+			if (kSDIYCbCrCaptureConfigs.find(swf) !=
+			    kSDIYCbCrCaptureConfigs.end()) {
+				routing = kSDIYCbCrCaptureConfigs.at(swf);
+				return true;
+			}
+		} else if (mode == NTV2_MODE_DISPLAY) {
+			if (kSDIYCbCrDisplayConfigs.find(swf) !=
+			    kSDIYCbCrDisplayConfigs.end()) {
+				routing = kSDIYCbCrDisplayConfigs.at(swf);
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+void Routing::StartSourceAudio(const SourceProps &props, CNTV2Card *card)
+{
+	if (!card)
+		return;
+
+	auto inputSrc = props.inputSource;
+	auto channel = props.Channel();
+	auto audioSys = props.AudioSystem();
+
+	card->WriteAudioSource(0, channel);
+	card->SetAudioSystemInputSource(
+		audioSys, NTV2InputSourceToAudioSource(inputSrc),
+		NTV2InputSourceToEmbeddedAudioInput(inputSrc));
+
+	card->SetNumberAudioChannels(props.audioNumChannels, audioSys);
+	card->SetAudioRate(props.AudioRate(), audioSys);
+	card->SetAudioBufferSize(NTV2_AUDIO_BUFFER_BIG, audioSys);
+
+	// Fix for AJA NTV2 internal bug #11467
+	ULWord magicAudioBits = 0;
+	if (NTV2_INPUT_SOURCE_IS_HDMI(inputSrc)) {
+		magicAudioBits = 0x00100000;
+		switch (inputSrc) {
+		default:
+		case NTV2_INPUTSOURCE_HDMI1:
+			magicAudioBits = 0x00100000;
+			break;
+		case NTV2_INPUTSOURCE_HDMI2:
+			magicAudioBits = 0x00110000;
+			break;
+		case NTV2_INPUTSOURCE_HDMI3:
+			magicAudioBits = 0x00900000;
+			break;
+		case NTV2_INPUTSOURCE_HDMI4:
+			magicAudioBits = 0x00910000;
+			break;
+		}
+	} else if (NTV2_INPUT_SOURCE_IS_ANALOG(inputSrc)) {
+		magicAudioBits = 0x00000990;
+	} else { // SDI
+		magicAudioBits = 0x00000320;
+	}
+
+	// TODO(paulh): Ask aja-seanl about these deprecated calls and if they are still needed
+	ULWord oldValue = 0;
+	if (card->ReadAudioSource(oldValue, channel)) {
+		card->WriteAudioSource(oldValue | magicAudioBits, channel);
+	}
+
+	for (int a = 0; a < NTV2DeviceGetNumAudioSystems(card->GetDeviceID());
+	     a++) {
+		card->SetAudioLoopBack(NTV2_AUDIO_LOOPBACK_OFF,
+				       NTV2AudioSystem(a));
+	}
+
+	card->StartAudioInput(audioSys);
+	card->SetAudioCaptureEnable(audioSys, true);
+}
+
+void Routing::StopSourceAudio(const SourceProps &props, CNTV2Card *card)
+{
+	if (card) {
+		auto audioSys = props.AudioSystem();
+		card->SetAudioCaptureEnable(audioSys, false);
+		card->StopAudioInput(audioSys);
+	}
+}
+
+// Guess an SDIWireFormat based on specified Video Format, IOSelection, 4K Transport and device ID.
+SDIWireFormat GuessSDIWireFormat(NTV2VideoFormat vf, IOSelection io,
+				 SDI4KTransport t4k,
+				 NTV2DeviceID device_id = DEVICE_ID_NOTFOUND)
+{
+	auto rd = GetRasterDefinition(io, vf, device_id);
+	auto fg = GetNTV2FrameGeometryFromVideoFormat(vf);
+
+	SDIWireFormat swf = SDIWireFormat::Unknown;
+	if (rd == RasterDefinition::SD) {
+		swf = SDIWireFormat::SD_ST352;
+	} else if (rd == RasterDefinition::HD) {
+		if (fg == NTV2_FG_1280x720) {
+			swf = SDIWireFormat::HD_720p_ST292;
+		} else if (fg == NTV2_FG_1920x1080 || fg == NTV2_FG_2048x1080) {
+			swf = SDIWireFormat::HD_1080_ST292;
+		}
+	} else if (rd == RasterDefinition::UHD_4K) {
+		if (t4k == SDI4KTransport::Squares) {
+			if (aja::IsSDIFourWireIOSelection(io)) {
+				swf = SDIWireFormat::UHD4K_ST292_Quad_1_5_Squares;
+			} else if (aja::IsSDITwoWireIOSelection(io)) {
+				if (t4k == SDI4KTransport::Squares) {
+					swf = SDIWireFormat::
+						UHD4K_ST292_Dual_1_5_Squares;
+				} else {
+					swf = SDIWireFormat::
+						UHD4K_ST425_Dual_3Gb_2SI;
+				}
+			}
+		} else if (t4k == SDI4KTransport::TwoSampleInterleave) {
+			if (aja::IsSDIOneWireIOSelection(io)) {
+				if (NTV2_IS_4K_HFR_VIDEO_FORMAT(vf)) {
+					if (aja::IsRetailSDI12G(device_id)) {
+						swf = SDIWireFormat::
+							UHD4K_ST2018_12G_Squares_2SI_Kona5_io4KPlus;
+					} else {
+						swf = SDIWireFormat::
+							UHD4K_ST2018_12G_Squares_2SI;
+					}
+				} else {
+					if (aja::IsRetailSDI12G(device_id)) {
+						swf = SDIWireFormat::
+							UHD4K_ST2018_6G_Squares_2SI_Kona5_io4KPlus;
+					} else {
+						swf = SDIWireFormat::
+							UHD4K_ST2018_6G_Squares_2SI;
+					}
+				}
+			} else if (aja::IsSDITwoWireIOSelection(io)) {
+				swf = SDIWireFormat::UHD4K_ST425_Dual_3Gb_2SI;
+			} else if (aja::IsSDIFourWireIOSelection(io)) {
+				swf = SDIWireFormat::UHD4K_ST425_Quad_3Gb_2SI;
+			}
+		}
+	}
+	return swf;
+}
+
+bool Routing::ConfigureSourceRoute(const SourceProps &props, NTV2Mode mode,
+				   CNTV2Card *card)
+{
+	if (!card)
+		return false;
+
+	auto deviceID = props.deviceID;
+
+	NTV2VideoFormat vf = props.videoFormat;
+	if (NTV2_VIDEO_FORMAT_IS_B(props.videoFormat)) {
+		vf = aja::GetLevelAFormatForLevelBFormat(props.videoFormat);
+	}
+
+	NTV2InputSourceSet inputSources;
+	aja::IOSelectionToInputSources(props.ioSelect, inputSources);
+	if (inputSources.empty()) {
+		blog(LOG_DEBUG,
+		     "No Input Sources specified to configure routing!");
+		return false;
+	}
+	auto init_src = *inputSources.begin();
+	auto init_channel = NTV2InputSourceToChannel(init_src);
+
+	RoutingConfig rc;
+	if (NTV2_INPUT_SOURCE_IS_SDI(init_src)) {
+		SDIWireFormat swf = SDIWireFormat::Unknown;
+		auto standard = VPIDStandard_Unknown;
+		auto vpidList = props.vpids;
+		if (vpidList.size() > 0)
+			standard = vpidList.at(0).Standard();
+
+		if (standard != VPIDStandard_Unknown) {
+			// Determine SDI format based on raster "definition" and VPID byte 1 value (AKA SMPTE standard)
+			auto rasterDef = GetRasterDefinition(props.ioSelect, vf,
+							     props.deviceID);
+			VPIDSpec vpidSpec = std::make_pair(rasterDef, standard);
+			DetermineSDIWireFormat(deviceID, vpidSpec, swf);
+		} else {
+			// Best guess SDI format from incoming video format if no VPIDs detected
+			swf = GuessSDIWireFormat(vf, props.ioSelect,
+						 props.sdi4kTransport,
+						 props.deviceID);
+		}
+
+		if (swf == SDIWireFormat::Unknown) {
+			blog(LOG_DEBUG, "Could not determine SDI Wire Format!");
+			return false;
+		}
+
+		if (!FindRoutingConfigSDI(swf, mode,
+					  NTV2_IS_FBF_RGB(props.pixelFormat),
+					  props.deviceID, rc)) {
+			blog(LOG_DEBUG,
+			     "Could not find RoutingConfig for SDI Wire Format!");
+			return false;
+		}
+	} else if (NTV2_INPUT_SOURCE_IS_HDMI(init_src)) {
+		HDMIWireFormat hwf = HDMIWireFormat::Unknown;
+
+		if (NTV2_IS_FBF_RGB(props.pixelFormat)) {
+			if (NTV2_IS_HD_VIDEO_FORMAT(vf))
+				hwf = HDMIWireFormat::HD_RGB_LFR;
+		} else {
+			if (NTV2_IS_HD_VIDEO_FORMAT(vf))
+				hwf = HDMIWireFormat::HD_YCBCR_LFR;
+			else if (NTV2_IS_4K_VIDEO_FORMAT(vf))
+				hwf = HDMIWireFormat::UHD_4K_YCBCR_LFR;
+		}
+
+		if (!FindRoutingConfigHDMI(hwf, mode,
+					   NTV2_IS_FBF_RGB(props.pixelFormat),
+					   props.deviceID, rc)) {
+			blog(LOG_DEBUG,
+			     "Could not find RoutingConfig for HDMI Wire Format!");
+			return false;
+		}
+	}
+
+	// Substitute channel placeholders for actual indices
+	std::string route_string = rc.route_string;
+	ULWord start_channel_index = GetIndexForNTV2Channel(init_channel);
+	for (ULWord c = 0; c < 8; c++) {
+		std::string channel_placeholder =
+			std::string("{ch" + aja::to_string(c + 1) + "}");
+		route_string =
+			aja::replace(route_string, channel_placeholder,
+				     aja::to_string(start_channel_index++));
+	}
+
+	NTV2XptConnections cnx;
+	ParseRouteString(route_string, cnx);
+
+	card->ApplySignalRoute(cnx, false);
+
+	// Apply SDI widget settings
+	start_channel_index = GetIndexForNTV2Channel(init_channel);
+	for (uint32_t i = (uint32_t)start_channel_index;
+	     i < (start_channel_index + rc.num_wires); i++) {
+		NTV2Channel channel = GetNTV2ChannelForIndex(i);
+		if (::NTV2DeviceHasBiDirectionalSDI(deviceID)) {
+			card->SetSDITransmitEnable(channel,
+						   mode == NTV2_MODE_DISPLAY);
+		}
+		card->SetSDIOut3GEnable(channel, rc.enable_3g_out);
+		card->SetSDIOut3GbEnable(channel, rc.enable_3gb_out);
+		card->SetSDIOut6GEnable(channel, rc.enable_6g_out);
+		card->SetSDIOut12GEnable(channel, rc.enable_12g_out);
+		card->SetSDIInLevelBtoLevelAConversion((UWord)i,
+						       rc.convert_3g_in);
+		card->SetSDIOutLevelAtoLevelBConversion((UWord)i,
+							rc.convert_3g_out);
+		card->SetSDIOutRGBLevelAConversion((UWord)i,
+						   rc.enable_rgb_3ga_convert);
+	}
+
+	// Apply Framestore settings
+	for (uint32_t i = (uint32_t)start_channel_index;
+	     i < (start_channel_index + rc.num_framestores); i++) {
+		NTV2Channel channel = GetNTV2ChannelForIndex(i);
+		card->EnableChannel(channel);
+		card->SetMode(channel, mode);
+		card->SetVANCMode(NTV2_VANCMODE_OFF, channel);
+		card->SetVideoFormat(vf, false, false, channel);
+		card->SetFrameBufferFormat(channel, props.pixelFormat);
+		card->SetTsiFrameEnable(rc.enable_tsi, channel);
+		card->Set4kSquaresEnable(rc.enable_4k_squares, channel);
+		card->SetQuadQuadSquaresEnable(rc.enable_8k_squares, channel);
+	}
+
+	return true;
+}
+
+bool Routing::ConfigureOutputRoute(const OutputProps &props, NTV2Mode mode,
+				   CNTV2Card *card)
+{
+	if (!card)
+		return false;
+
+	auto deviceID = props.deviceID;
+
+	NTV2OutputDestinations outputDests;
+	aja::IOSelectionToOutputDests(props.ioSelect, outputDests);
+	if (outputDests.empty()) {
+		blog(LOG_DEBUG,
+		     "No Output Destinations specified to configure routing!");
+		return false;
+	}
+
+	auto init_dest = *outputDests.begin();
+	auto init_channel = NTV2OutputDestinationToChannel(init_dest);
+
+	RoutingConfig rc;
+	if (NTV2_OUTPUT_DEST_IS_SDI(init_dest)) {
+		SDIWireFormat swf = GuessSDIWireFormat(props.videoFormat,
+						       props.ioSelect,
+						       props.sdi4kTransport,
+						       props.deviceID);
+
+		if (swf == SDIWireFormat::Unknown) {
+			blog(LOG_DEBUG, "Could not determine SDI Wire Format!");
+			return false;
+		}
+
+		if (!FindRoutingConfigSDI(swf, mode,
+					  NTV2_IS_FBF_RGB(props.pixelFormat),
+					  props.deviceID, rc)) {
+			blog(LOG_DEBUG,
+			     "Could not find RoutingConfig for SDI Wire Format!");
+			return false;
+		}
+	} else if (NTV2_OUTPUT_DEST_IS_HDMI(init_dest)) {
+		HDMIWireFormat hwf = HDMIWireFormat::Unknown;
+
+		// special case devices...
+		if (props.deviceID == DEVICE_ID_TTAP_PRO) {
+			hwf = HDMIWireFormat::TTAP_PRO;
+		} else {
+			// ...all other devices.
+			if (NTV2_IS_FBF_RGB(props.pixelFormat)) {
+				if (NTV2_IS_HD_VIDEO_FORMAT(props.videoFormat))
+					hwf = HDMIWireFormat::HD_RGB_LFR;
+			} else {
+				if (NTV2_IS_HD_VIDEO_FORMAT(
+					    props.videoFormat)) {
+					hwf = HDMIWireFormat::HD_YCBCR_LFR;
+				} else if (NTV2_IS_4K_VIDEO_FORMAT(
+						   props.videoFormat)) {
+					hwf = HDMIWireFormat::UHD_4K_YCBCR_LFR;
+				}
+			}
+		}
+
+		if (!FindRoutingConfigHDMI(hwf, mode,
+					   NTV2_IS_FBF_RGB(props.pixelFormat),
+					   props.deviceID, rc)) {
+			blog(LOG_DEBUG,
+			     "Could not find RoutingConfig for HDMI Wire Format!");
+			return false;
+		}
+	}
+
+	std::string route_string = rc.route_string;
+
+	// Replace framestore channel placeholders
+	ULWord start_framestore_index = initial_framestore_output_index(
+		deviceID, props.ioSelect, init_channel);
+	for (ULWord c = 0; c < NTV2_MAX_NUM_CHANNELS; c++) {
+		std::string fs_channel_placeholder =
+			std::string("fb[{ch" + aja::to_string(c + 1) + "}]");
+		route_string = aja::replace(
+			route_string, fs_channel_placeholder,
+			"fb[" + aja::to_string(start_framestore_index++) + "]");
+	}
+
+	// Replace other channel placeholders
+	ULWord start_channel_index = GetIndexForNTV2Channel(init_channel);
+	for (ULWord c = 0; c < NTV2_MAX_NUM_CHANNELS; c++) {
+		std::string channel_placeholder =
+			std::string("{ch" + aja::to_string(c + 1) + "}");
+		route_string =
+			aja::replace(route_string, channel_placeholder,
+				     aja::to_string(start_channel_index++));
+	}
+
+	NTV2XptConnections cnx;
+	ParseRouteString(route_string, cnx);
+	card->ApplySignalRoute(cnx, false);
+
+	// Apply SDI widget settings
+	if (props.ioSelect != IOSelection::HDMIMonitorOut) {
+		start_channel_index = GetIndexForNTV2Channel(init_channel);
+		for (uint32_t i = (uint32_t)start_channel_index;
+		     i < (start_channel_index + rc.num_wires); i++) {
+			NTV2Channel channel = GetNTV2ChannelForIndex(i);
+			if (::NTV2DeviceHasBiDirectionalSDI(deviceID)) {
+				card->SetSDITransmitEnable(
+					channel, mode == NTV2_MODE_DISPLAY);
+			}
+			card->SetSDIOut3GEnable(channel, rc.enable_3g_out);
+			card->SetSDIOut3GbEnable(channel, rc.enable_3gb_out);
+			card->SetSDIOut6GEnable(channel, rc.enable_6g_out);
+			card->SetSDIOut12GEnable(channel, rc.enable_12g_out);
+			card->SetSDIInLevelBtoLevelAConversion(
+				(UWord)i, rc.convert_3g_in);
+			card->SetSDIOutLevelAtoLevelBConversion(
+				(UWord)i, rc.convert_3g_out);
+			card->SetSDIOutRGBLevelAConversion(
+				(UWord)i, rc.enable_rgb_3ga_convert);
+		}
+	}
+
+	// Apply Framestore settings
+	start_framestore_index = initial_framestore_output_index(
+		deviceID, props.ioSelect, init_channel);
+	for (uint32_t i = (uint32_t)start_framestore_index;
+	     i < (start_framestore_index + rc.num_framestores); i++) {
+		NTV2Channel channel = GetNTV2ChannelForIndex(i);
+		card->EnableChannel(channel);
+		card->SetMode(channel, mode);
+		card->SetVANCMode(NTV2_VANCMODE_OFF, channel);
+		card->SetVideoFormat(props.videoFormat, false, false, channel);
+		card->SetFrameBufferFormat(channel, props.pixelFormat);
+		card->SetTsiFrameEnable(rc.enable_tsi, channel);
+		card->Set4kSquaresEnable(rc.enable_4k_squares, channel);
+		card->SetQuadQuadSquaresEnable(rc.enable_8k_squares, channel);
+	}
+
+	return true;
+}
+
+ULWord Routing::initial_framestore_output_index(NTV2DeviceID deviceID,
+						IOSelection io,
+						NTV2Channel init_channel)
+{
+	if (deviceID == DEVICE_ID_TTAP_PRO) {
+		return 0;
+	} else if (deviceID == DEVICE_ID_KONA1) {
+		return 1;
+	} else if (deviceID == DEVICE_ID_IO4K ||
+		   deviceID == DEVICE_ID_IO4KPLUS) {
+		// SDI Monitor output uses framestore 4
+		if (io == IOSelection::SDI5)
+			return 3;
+	}
+
+	// HDMI Monitor output uses framestore 4
+	if (io == IOSelection::HDMIMonitorOut) {
+		return 3;
+	}
+
+	return GetIndexForNTV2Channel(init_channel);
+}
+
+// Output Routing
+void Routing::ConfigureOutputAudio(const OutputProps &props, CNTV2Card *card)
+{
+	if (!card)
+		return;
+
+	auto deviceID = card->GetDeviceID();
+	auto audioSys = props.AudioSystem();
+	auto channel = props.Channel();
+
+	card->SetNumberAudioChannels(props.audioNumChannels, audioSys);
+	card->SetAudioRate(props.AudioRate(), audioSys);
+	card->SetAudioBufferSize(NTV2_AUDIO_BUFFER_BIG, audioSys);
+	card->SetAudioOutputDelay(audioSys, 0);
+
+	card->SetSDIOutputAudioSystem(channel, audioSys);
+	card->SetSDIOutputDS2AudioSystem(channel, audioSys);
+
+	/* NOTE(paulh):
+	 * The SDK has a specifies an SDI audio system by Channel rather than by SDI output
+	 * and certain devices require setting the SDI audio system to NTV2_CHANNEL1.
+	 * i.e.
+	 * SDI 1 = NTV2_CHANNEL1
+	 * SDI 2 = NTV2_CHANNEL2
+	 * ...
+	 * SDI 5/Monitor = NTV2_CHANNEL5
+	 * etc...
+	 *
+	 * This fixes AJA internal bugs: 10730, 10986, 16274
+	 */
+	if (deviceID == DEVICE_ID_IOXT || deviceID == DEVICE_ID_IO4KUFC ||
+	    deviceID == DEVICE_ID_IO4KPLUS || deviceID == DEVICE_ID_KONA1 ||
+	    deviceID == DEVICE_ID_KONA3G || deviceID == DEVICE_ID_KONA4UFC ||
+	    deviceID == DEVICE_ID_KONA5 || deviceID == DEVICE_ID_KONA5_2X4K) {
+		// Make sure SDI out 1 (aka Channel 1) is set to the correct sub-system
+		card->SetSDIOutputAudioSystem(NTV2_CHANNEL1, audioSys);
+		card->SetSDIOutputDS2AudioSystem(NTV2_CHANNEL1, audioSys);
+	}
+
+	// make sure that audio is setup for the SDI monitor output on devices that support it
+	if (NTV2DeviceCanDoWidget(deviceID, NTV2_WgtSDIMonOut1)) {
+		card->SetSDIOutputAudioSystem(NTV2_CHANNEL5, audioSys);
+		card->SetSDIOutputDS2AudioSystem(NTV2_CHANNEL5, audioSys);
+	}
+
+	card->SetHDMIOutAudioRate(props.AudioRate());
+	card->SetHDMIOutAudioFormat(NTV2_AUDIO_FORMAT_LPCM);
+
+	card->SetAudioOutputMonitorSource(NTV2_AudioChannel1_2, channel);
+	card->SetAESOutputSource(NTV2_AudioChannel1_4, audioSys,
+				 NTV2_AudioChannel1_4);
+	card->SetAESOutputSource(NTV2_AudioChannel5_8, audioSys,
+				 NTV2_AudioChannel5_8);
+	card->SetAESOutputSource(NTV2_AudioChannel9_12, audioSys,
+				 NTV2_AudioChannel9_12);
+	card->SetAESOutputSource(NTV2_AudioChannel13_16, audioSys,
+				 NTV2_AudioChannel13_16);
+
+	// make sure that audio is setup for HDMI output on devices that support it
+	if (NTV2DeviceGetNumHDMIVideoOutputs(deviceID) > 0) {
+		if (NTV2DeviceCanDoAudioMixer(deviceID)) {
+			card->SetAudioMixerInputAudioSystem(
+				NTV2_AudioMixerInputMain, audioSys);
+			card->SetAudioMixerInputChannelSelect(
+				NTV2_AudioMixerInputMain, NTV2_AudioChannel1_2);
+			card->SetAudioMixerInputChannelsMute(
+				NTV2_AudioMixerInputAux1,
+				NTV2AudioChannelsMuteAll);
+			card->SetAudioMixerInputChannelsMute(
+				NTV2_AudioMixerInputAux2,
+				NTV2AudioChannelsMuteAll);
+		}
+
+		card->SetHDMIOutAudioChannels(NTV2_HDMIAudio8Channels);
+		card->SetHDMIOutAudioSource2Channel(NTV2_AudioChannel1_2,
+						    audioSys);
+		card->SetHDMIOutAudioSource8Channel(NTV2_AudioChannel1_8,
+						    audioSys);
+	}
+
+	card->SetAudioLoopBack(NTV2_AUDIO_LOOPBACK_OFF, audioSys);
+
+	card->StopAudioOutput(audioSys);
+}

+ 139 - 0
plugins/aja/aja-routing.hpp

@@ -0,0 +1,139 @@
+#pragma once
+#include "aja-props.hpp"
+
+#include <ajantv2/includes/ntv2enums.h>
+
+#include <iostream>
+#include <string>
+#include <map>
+#include <vector>
+
+class CNTV2Card;
+
+/* The AJA hardware and NTV2 SDK uses a concept called "Signal Routing" to connect high-level firmware
+ * blocks known as "Widgets" to one another via "crosspoint" connections. This facilitates streaming
+ * data from one Widget to another to achieve specific functionality.
+ * Such functionality may include SDI/HDMI capture/output, colorspace conversion, hardware LUTs, etc.
+ * 
+ * This code references a table of RoutingConfig entries, where each entry contains the settings required
+ * to configure an AJA device for a particular capture or output task. These settings include the number of
+ * physical IO Widgets (SDI or HDMI) required, number of framestore Widgets required, register settings
+ * that must be enabled or disabled, and a special short-hand "route string".
+ * Of special note is the route string, which is parsed into a map of NTV2XptConnections. These connections
+ * are then applied as the "signal route", connecting the Widget's crosspoints together.
+ */
+
+struct RoutingConfig {
+	NTV2Mode mode;            // capture or playout?
+	uint32_t num_wires;       // number of physical connections
+	uint32_t num_framestores; // number of framestores used
+	bool enable_3g_out;       // enable register for 3G SDI Output?
+	bool enable_6g_out;       // enable register for 6G SDI Output?
+	bool enable_12g_out;      // enable register for 12G SDI Output?
+	bool convert_3g_in; // enable register for 3G level-B -> level-A SDI input conversion?
+	bool convert_3g_out; // enable register for 3G level-A -> level-B SDI output conversion?
+	bool enable_rgb_3ga_convert; // enable register for RGB 3G level-B -> level-A SDI output conversion?
+	bool enable_3gb_out;    // enable register for 3G level-B SDI output?
+	bool enable_4k_squares; // enable register for 4K square division?
+	bool enable_8k_squares; // enable register for 8K square division?
+	bool enable_tsi; // enable register for two-sample interleave (UHD/4K/8K)
+	std::string
+		route_string; // signal routing shorthand string to parse into crosspoint connections
+};
+
+/* This table is used to correlate a particular "raster definition" (i.e. SD/HD/4K/etc.)
+ * and SMPTE VPID transport byte (VPIDStandard) to an SDIWireFormat enum.
+ * This allows mapping SDI video signals to the correct format, particularly in the case
+ * where multiple SDI formats share the same VPID transport value.
+ * For example: VPIDStandard_1080 (0x85) is used on the wire for both single-link (1x SDI wire)
+ * 1080-line HD SDI video AND quad-link (4x SDI wires) UHD/4K "square-division" video.
+ */
+using VPIDSpec = std::pair<RasterDefinition, VPIDStandard>;
+
+static const std::map<VPIDSpec, SDIWireFormat> kSDIWireFormats = {
+	{{RasterDefinition::SD, VPIDStandard_483_576}, SDIWireFormat::SD_ST352},
+	{{RasterDefinition::HD, VPIDStandard_720},
+	 SDIWireFormat::HD_720p_ST292},
+	{{RasterDefinition::HD, VPIDStandard_1080},
+	 SDIWireFormat::HD_1080_ST292},
+	{{RasterDefinition::HD, VPIDStandard_1080_DualLink},
+	 SDIWireFormat::HD_1080_ST372_Dual},
+	{{RasterDefinition::HD, VPIDStandard_720_3Ga},
+	 SDIWireFormat::HD_720p_ST425_3Ga},
+	{{RasterDefinition::HD, VPIDStandard_1080_3Ga},
+	 SDIWireFormat::HD_1080p_ST425_3Ga},
+	{{RasterDefinition::HD, VPIDStandard_1080_DualLink_3Gb},
+	 SDIWireFormat::HD_1080p_ST425_3Gb_DL},
+	{{RasterDefinition::HD, VPIDStandard_720_3Gb},
+	 SDIWireFormat::HD_720p_ST425_3Gb},
+	{{RasterDefinition::HD, VPIDStandard_1080_3Gb},
+	 SDIWireFormat::HD_1080p_ST425_3Gb},
+	{{RasterDefinition::HD, VPIDStandard_1080_Dual_3Ga},
+	 SDIWireFormat::HD_1080p_ST425_Dual_3Ga},
+	{{RasterDefinition::HD, VPIDStandard_1080_Dual_3Gb},
+	 SDIWireFormat::HD_1080p_ST425_Dual_3Gb},
+	{{RasterDefinition::UHD_4K, VPIDStandard_1080_3Gb},
+	 SDIWireFormat::UHD4K_ST292_Dual_1_5_Squares},
+	{{RasterDefinition::UHD_4K, VPIDStandard_1080},
+	 SDIWireFormat::UHD4K_ST292_Quad_1_5_Squares},
+	{{RasterDefinition::UHD_4K, VPIDStandard_1080_3Ga},
+	 SDIWireFormat::UHD4K_ST425_Quad_3Ga_Squares},
+	{{RasterDefinition::UHD_4K, VPIDStandard_1080_DualLink_3Gb},
+	 SDIWireFormat::UHD4K_ST425_Quad_3Gb_Squares},
+	{{RasterDefinition::UHD_4K, VPIDStandard_2160_DualLink},
+	 SDIWireFormat::UHD4K_ST425_Dual_3Gb_2SI},
+	{{RasterDefinition::UHD_4K, VPIDStandard_2160_QuadLink_3Ga},
+	 SDIWireFormat::UHD4K_ST425_Quad_3Ga_2SI},
+	{{RasterDefinition::UHD_4K, VPIDStandard_2160_QuadDualLink_3Gb},
+	 SDIWireFormat::UHD4K_ST425_Quad_3Gb_2SI},
+	{{RasterDefinition::UHD_4K, VPIDStandard_2160_Single_6Gb},
+	 SDIWireFormat::UHD4K_ST2018_6G_Squares_2SI},
+	{{RasterDefinition::UHD_4K_Retail_12G, VPIDStandard_2160_Single_6Gb},
+	 SDIWireFormat::UHD4K_ST2018_6G_Squares_2SI_Kona5_io4KPlus},
+	{{RasterDefinition::UHD_4K, VPIDStandard_2160_Single_12Gb},
+	 SDIWireFormat::UHD4K_ST2018_12G_Squares_2SI},
+	{{RasterDefinition::UHD_4K_Retail_12G, VPIDStandard_2160_Single_12Gb},
+	 SDIWireFormat::UHD4K_ST2018_12G_Squares_2SI_Kona5_io4KPlus},
+	{{RasterDefinition::UHD2_8K, VPIDStandard_4320_DualLink_12Gb},
+	 SDIWireFormat::UHD28K_ST2082_Dual_12G},
+	{{RasterDefinition::UHD2_8K, VPIDStandard_2160_DualLink_12Gb},
+	 SDIWireFormat::UHD28K_ST2082_RGB_Dual_12G},
+	{{RasterDefinition::UHD2_8K, VPIDStandard_4320_QuadLink_12Gb},
+	 SDIWireFormat::UHD28K_ST2082_Quad_12G},
+};
+
+extern RasterDefinition
+GetRasterDefinition(IOSelection io, NTV2VideoFormat vf,
+		    NTV2DeviceID deviceID = DEVICE_ID_NOTFOUND);
+
+extern std::string RasterDefinitionToString(RasterDefinition rd);
+
+// Applies RoutingConfig settings to the card to configure a specific SDI/HDMI capture/output mode.
+class Routing {
+public:
+	static bool ParseRouteString(const std::string &route,
+				     NTV2XptConnections &cnx);
+	static bool DetermineSDIWireFormat(NTV2DeviceID deviceID, VPIDSpec spec,
+					   SDIWireFormat &swf);
+	static bool FindRoutingConfigHDMI(HDMIWireFormat hwf, NTV2Mode mode,
+					  bool isRGB, NTV2DeviceID deviceID,
+					  RoutingConfig &routing);
+	static bool FindRoutingConfigSDI(SDIWireFormat swf, NTV2Mode mode,
+					 bool isRGB, NTV2DeviceID deviceID,
+					 RoutingConfig &routing);
+
+	static void StartSourceAudio(const SourceProps &props, CNTV2Card *card);
+
+	static void StopSourceAudio(const SourceProps &props, CNTV2Card *card);
+
+	static bool ConfigureSourceRoute(const SourceProps &props,
+					 NTV2Mode mode, CNTV2Card *card);
+	static bool ConfigureOutputRoute(const OutputProps &props,
+					 NTV2Mode mode, CNTV2Card *card);
+	static ULWord initial_framestore_output_index(NTV2DeviceID deviceID,
+						      IOSelection io,
+						      NTV2Channel init_channel);
+
+	static void ConfigureOutputAudio(const OutputProps &props,
+					 CNTV2Card *card);
+};

+ 1184 - 0
plugins/aja/aja-source.cpp

@@ -0,0 +1,1184 @@
+#include "aja-card-manager.hpp"
+#include "aja-common.hpp"
+#include "aja-ui-props.hpp"
+#include "aja-source.hpp"
+#include "aja-routing.hpp"
+
+#include <util/threading.h>
+#include <util/platform.h>
+#include <util/dstr.h>
+#include <obs-module.h>
+
+#include <ajantv2/includes/ntv2card.h>
+
+#define NTV2_AUDIOSIZE_MAX (401 * 1024)
+
+AJASource::AJASource(obs_source_t *source)
+	: mVideoBuffer{},
+	  mAudioBuffer{},
+	  mCard{nullptr},
+	  mSourceName{""},
+	  mCardID{""},
+	  mDeviceIndex{0},
+	  mBuffering{false},
+	  mIsCapturing{false},
+	  mSourceProps{},
+	  mTestPattern{},
+	  mCaptureThread{nullptr},
+	  mMutex{},
+	  mSource{source}
+{
+}
+
+AJASource::~AJASource()
+{
+	Deactivate();
+	mTestPattern.clear();
+	mVideoBuffer.Deallocate();
+	mAudioBuffer.Deallocate();
+	mVideoBuffer = NULL;
+	mAudioBuffer = NULL;
+}
+
+void AJASource::SetCard(CNTV2Card *card)
+{
+	mCard = card;
+}
+
+CNTV2Card *AJASource::GetCard()
+{
+	return mCard;
+}
+
+void AJASource::SetOBSSource(obs_source_t *source)
+{
+	mSource = source;
+}
+
+obs_source_t *AJASource::GetOBSSource(void) const
+{
+	return mSource;
+}
+
+void AJASource::SetName(const std::string &name)
+{
+	mSourceName = name;
+}
+
+std::string AJASource::GetName() const
+{
+	return mSourceName;
+}
+
+void populate_source_device_list(obs_property_t *list)
+{
+	obs_property_list_clear(list);
+
+	auto &cardManager = aja::CardManager::Instance();
+	cardManager.EnumerateCards();
+
+	for (const auto &iter : cardManager.GetCardEntries()) {
+		if (iter.second) {
+			CNTV2Card *card = iter.second->GetCard();
+			if (!card)
+				continue;
+
+			if (aja::IsOutputOnlyDevice(iter.second->GetDeviceID()))
+				continue;
+
+			obs_property_list_add_string(
+				list, iter.second->GetDisplayName().c_str(),
+				iter.second->GetCardID().c_str());
+		}
+	}
+}
+
+//
+// Capture Thread stuff
+//
+
+struct AudioOffsets {
+	ULWord currentAddress = 0;
+	ULWord lastAddress = 0;
+	ULWord readOffset = 0;
+	ULWord wrapAddress = 0;
+	ULWord bytesRead = 0;
+};
+
+static void ResetAudioBufferOffsets(CNTV2Card *card,
+				    NTV2AudioSystem audioSystem,
+				    AudioOffsets &offsets)
+{
+	if (!card)
+		return;
+
+	offsets.currentAddress = 0;
+	offsets.lastAddress = 0;
+	offsets.readOffset = 0;
+	offsets.wrapAddress = 0;
+	offsets.bytesRead = 0;
+
+	card->GetAudioReadOffset(offsets.readOffset, audioSystem);
+	card->GetAudioWrapAddress(offsets.wrapAddress, audioSystem);
+	offsets.wrapAddress += offsets.readOffset;
+
+	offsets.lastAddress = offsets.readOffset;
+}
+
+void AJASource::GenerateTestPattern(NTV2VideoFormat vf, NTV2PixelFormat pf,
+				    NTV2TestPatternSelect ps)
+{
+	NTV2VideoFormat vid_fmt = vf;
+	NTV2PixelFormat pix_fmt = pf;
+
+	if (vid_fmt == NTV2_FORMAT_UNKNOWN)
+		vid_fmt = NTV2_FORMAT_720p_5994;
+	if (pix_fmt == NTV2_FBF_INVALID)
+		pix_fmt = kDefaultAJAPixelFormat;
+
+	NTV2FormatDesc fd(vid_fmt, pix_fmt, NTV2_VANCMODE_OFF);
+	auto bufSize = fd.GetTotalRasterBytes();
+
+	if (bufSize != mTestPattern.size()) {
+		mTestPattern.clear();
+		mTestPattern.resize(bufSize);
+
+		NTV2TestPatternGen gen;
+		gen.DrawTestPattern(ps, fd.GetRasterWidth(),
+				    fd.GetRasterHeight(), pix_fmt,
+				    mTestPattern);
+	}
+
+	if (mTestPattern.size() == 0) {
+		blog(LOG_DEBUG,
+		     "AJASource::GenerateTestPattern: Error generating test pattern!");
+		return;
+	}
+
+	struct obs_source_frame2 obsFrame;
+	obsFrame.flip = false;
+	obsFrame.timestamp = os_gettime_ns();
+	obsFrame.width = fd.GetRasterWidth();
+	obsFrame.height = fd.GetRasterHeight();
+	obsFrame.format = aja::AJAPixelFormatToOBSVideoFormat(pix_fmt);
+	obsFrame.data[0] = mTestPattern.data();
+	obsFrame.linesize[0] = fd.GetBytesPerRow();
+
+	video_format_get_parameters(VIDEO_CS_DEFAULT, VIDEO_RANGE_FULL,
+				    obsFrame.color_matrix,
+				    obsFrame.color_range_min,
+				    obsFrame.color_range_max);
+
+	obs_source_output_video2(mSource, &obsFrame);
+}
+
+void AJASource::CaptureThread(AJAThread *thread, void *data)
+{
+	UNUSED_PARAMETER(thread);
+
+	auto ajaSource = (AJASource *)data;
+	if (!ajaSource) {
+		blog(LOG_WARNING,
+		     "AJASource::CaptureThread: Plugin instance is null!");
+		return;
+	}
+
+	blog(LOG_INFO,
+	     "AJASource::CaptureThread: Starting capture thread for AJA source %s",
+	     ajaSource->GetName().c_str());
+
+	auto card = ajaSource->GetCard();
+	if (!card) {
+		blog(LOG_ERROR,
+		     "AJASource::CaptureThread: Card instance is null!");
+		return;
+	}
+
+	auto sourceProps = ajaSource->GetSourceProps();
+	auto channel = sourceProps.Channel();
+	auto audioSystem = sourceProps.AudioSystem();
+
+	// Current "on-air" frame on the card. The capture thread "Ping-pongs" between
+	// two frames, starting at an index corresponding to the framestore channel.
+	// For example:
+	// Channel 1 (index 0) = frames 0/1
+	// Channel 2 (index 1) = frames 2/3
+	// Channel 3 (index 2) = frames 4/5
+	// Channel 4 (index 3) = frames 6/7
+	// etc...
+	ULWord currentCardFrame = (uint32_t)channel * 2;
+	card->WaitForInputFieldID(NTV2_FIELD0, channel);
+
+	currentCardFrame ^= 1;
+
+	card->SetInputFrame(channel, currentCardFrame);
+
+	AudioOffsets offsets;
+	ResetAudioBufferOffsets(card, audioSystem, offsets);
+
+	obs_data_t *settings = obs_source_get_settings(ajaSource->mSource);
+
+	while (ajaSource->IsCapturing()) {
+		if (card->GetModelName() == "(Not Found)") {
+			os_sleep_ms(250);
+
+			obs_source_update(ajaSource->mSource, settings);
+
+			break;
+		}
+
+		bool audioOverrun = false;
+
+		card->WaitForInputFieldID(NTV2_FIELD0, channel);
+
+		currentCardFrame ^= 1;
+
+		auto videoFormat = sourceProps.videoFormat;
+		auto pixelFormat = sourceProps.pixelFormat;
+
+		// Card format detection -- restarts capture thread via aja_source_update callback
+		auto newVideoFormat = card->GetInputVideoFormat(
+			sourceProps.inputSource,
+			aja::Is3GLevelB(card, channel));
+
+		if (newVideoFormat == NTV2_FORMAT_UNKNOWN) {
+			blog(LOG_DEBUG,
+			     "AJASource::CaptureThread: Video format unknown!");
+			ajaSource->GenerateTestPattern(videoFormat, pixelFormat,
+						       NTV2_TestPatt_Black);
+			os_sleep_ms(250);
+			continue;
+		}
+
+		if (sourceProps.autoDetect && (videoFormat != newVideoFormat)) {
+			blog(LOG_INFO,
+			     "AJASource::CaptureThread: New Video Format detected! Triggering 'aja_source_update' callback and returning...");
+			blog(LOG_INFO,
+			     "AJASource::CaptureThread: Current Video Format: %s, | Want Video Format: %s",
+			     NTV2VideoFormatToString(videoFormat, true).c_str(),
+			     NTV2VideoFormatToString(newVideoFormat, true)
+				     .c_str());
+
+			os_sleep_ms(250);
+
+			obs_source_update(ajaSource->mSource, settings);
+
+			break;
+		}
+
+		card->ReadAudioLastIn(offsets.currentAddress, audioSystem);
+		offsets.currentAddress &= ~0x3; // Force DWORD alignment
+		offsets.currentAddress += offsets.readOffset;
+
+		if (offsets.currentAddress < offsets.lastAddress) {
+			offsets.bytesRead =
+				offsets.wrapAddress - offsets.lastAddress;
+
+			if (offsets.bytesRead >
+			    ajaSource->mAudioBuffer.GetByteCount()) {
+
+				blog(LOG_DEBUG,
+				     "AJASource::CaptureThread: Audio overrun (1)! Buffer Size: %d, Bytes Captured: %d",
+				     ajaSource->mAudioBuffer.GetByteCount(),
+				     offsets.bytesRead);
+
+				ResetAudioBufferOffsets(card, audioSystem,
+							offsets);
+
+				audioOverrun = true;
+			}
+
+			if (!audioOverrun) {
+				card->DMAReadAudio(audioSystem,
+						   ajaSource->mAudioBuffer,
+						   offsets.lastAddress,
+						   offsets.bytesRead);
+
+				card->DMAReadAudio(
+					audioSystem,
+					reinterpret_cast<ULWord *>(
+						ajaSource->mAudioBuffer
+							.GetHostAddress(
+								offsets.bytesRead)),
+					offsets.readOffset,
+					offsets.currentAddress -
+						offsets.readOffset);
+
+				offsets.bytesRead += offsets.currentAddress -
+						     offsets.readOffset;
+			}
+
+			if (offsets.bytesRead >
+			    ajaSource->mAudioBuffer.GetByteCount()) {
+
+				blog(LOG_DEBUG,
+				     "AJASource::CaptureThread: Audio overrun (2)! Buffer Size: %d, Bytes Captured: %d",
+				     ajaSource->mAudioBuffer.GetByteCount(),
+				     offsets.bytesRead);
+
+				ResetAudioBufferOffsets(card, audioSystem,
+							offsets);
+
+				audioOverrun = true;
+			}
+		} else {
+			offsets.bytesRead =
+				offsets.currentAddress - offsets.lastAddress;
+
+			if (offsets.bytesRead >
+			    ajaSource->mAudioBuffer.GetByteCount()) {
+
+				blog(LOG_DEBUG,
+				     "AJASource::CaptureThread: Audio overrun (3)! Buffer Size: %d, Bytes Captured: %d",
+				     ajaSource->mAudioBuffer.GetByteCount(),
+				     offsets.bytesRead);
+
+				ResetAudioBufferOffsets(card, audioSystem,
+							offsets);
+
+				audioOverrun = true;
+			}
+
+			if (!audioOverrun) {
+				card->DMAReadAudio(audioSystem,
+						   ajaSource->mAudioBuffer,
+						   offsets.lastAddress,
+						   offsets.bytesRead);
+			}
+		}
+
+		if (!audioOverrun) {
+			offsets.lastAddress = offsets.currentAddress;
+
+			obs_source_audio audioPacket;
+			audioPacket.samples_per_sec = 48000;
+			audioPacket.format = AUDIO_FORMAT_32BIT;
+			audioPacket.speakers = SPEAKERS_7POINT1;
+			audioPacket.frames = offsets.bytesRead / 32;
+			audioPacket.timestamp = os_gettime_ns();
+			audioPacket.data[0] = (uint8_t *)ajaSource->mAudioBuffer
+						      .GetHostPointer();
+
+			obs_source_output_audio(ajaSource->mSource,
+						&audioPacket);
+		}
+
+		if (ajaSource->mVideoBuffer.GetByteCount() == 0) {
+			blog(LOG_DEBUG,
+			     "AJASource::CaptureThread: 0 bytes in video buffer! Something went wrong!");
+			continue;
+		}
+
+		card->DMAReadFrame(currentCardFrame, ajaSource->mVideoBuffer,
+				   ajaSource->mVideoBuffer.GetByteCount());
+
+		auto actualVideoFormat = videoFormat;
+		if (aja::Is3GLevelB(card, channel))
+			actualVideoFormat = aja::GetLevelAFormatForLevelBFormat(
+				videoFormat);
+
+		NTV2FormatDesc fd(actualVideoFormat, pixelFormat);
+
+		struct obs_source_frame2 obsFrame;
+		obsFrame.flip = false;
+		obsFrame.timestamp = os_gettime_ns();
+		obsFrame.width = fd.GetRasterWidth();
+		obsFrame.height = fd.GetRasterHeight();
+		obsFrame.format = aja::AJAPixelFormatToOBSVideoFormat(
+			sourceProps.pixelFormat);
+		obsFrame.data[0] = reinterpret_cast<uint8_t *>(
+			(ULWord *)ajaSource->mVideoBuffer.GetHostPointer());
+		obsFrame.linesize[0] = fd.GetBytesPerRow();
+
+		video_format_get_parameters(VIDEO_CS_DEFAULT, VIDEO_RANGE_FULL,
+					    obsFrame.color_matrix,
+					    obsFrame.color_range_min,
+					    obsFrame.color_range_max);
+
+		obs_source_output_video2(ajaSource->mSource, &obsFrame);
+
+		card->SetInputFrame(channel, currentCardFrame);
+	}
+
+	blog(LOG_INFO, "AJASource::Capturethread: Thread loop stopped");
+
+	ajaSource->GenerateTestPattern(sourceProps.videoFormat,
+				       sourceProps.pixelFormat,
+				       NTV2_TestPatt_Black);
+
+	obs_data_release(settings);
+}
+
+void AJASource::Deactivate()
+{
+	SetCapturing(false);
+
+	if (mCaptureThread) {
+		if (mCaptureThread->Active()) {
+			mCaptureThread->Stop();
+			blog(LOG_INFO, "AJASource::CaptureThread: Stopped!");
+		}
+		delete mCaptureThread;
+		mCaptureThread = nullptr;
+		blog(LOG_INFO, "AJASource::CaptureThread: Destroyed!");
+	}
+}
+
+void AJASource::Activate(bool enable)
+{
+	if (mCaptureThread == nullptr) {
+		mCaptureThread = new AJAThread();
+		mCaptureThread->Attach(AJASource::CaptureThread, this);
+		mCaptureThread->SetPriority(AJA_ThreadPriority_High);
+		blog(LOG_INFO, "AJASource::CaptureThread: Created!");
+	}
+
+	if (enable) {
+		SetCapturing(true);
+		if (!mCaptureThread->Active()) {
+			mCaptureThread->Start();
+			blog(LOG_INFO, "AJASource::CaptureThread: Started!");
+		}
+	}
+}
+
+bool AJASource::IsCapturing() const
+{
+	return mIsCapturing;
+}
+
+void AJASource::SetCapturing(bool capturing)
+{
+	std::lock_guard<std::mutex> lock(mMutex);
+	mIsCapturing = capturing;
+}
+
+//
+// CardEntry/Device stuff
+//
+std::string AJASource::CardID() const
+{
+	return mCardID;
+}
+void AJASource::SetCardID(const std::string &cardID)
+{
+	mCardID = cardID;
+}
+uint32_t AJASource::DeviceIndex() const
+{
+	return static_cast<uint32_t>(mDeviceIndex);
+}
+void AJASource::SetDeviceIndex(uint32_t index)
+{
+	mDeviceIndex = static_cast<UWord>(index);
+}
+
+//
+// AJASource Properties stuff
+//
+void AJASource::SetSourceProps(const SourceProps &props)
+{
+	mSourceProps = props;
+}
+
+SourceProps AJASource::GetSourceProps() const
+{
+	return mSourceProps;
+}
+
+bool AJASource::ReadChannelVPIDs(NTV2Channel channel, VPIDData &vpids)
+{
+	ULWord vpid_a = 0;
+	ULWord vpid_b = 0;
+	bool read_ok = mCard->ReadSDIInVPID(channel, vpid_a, vpid_b);
+	vpids.SetA(vpid_a);
+	vpids.SetB(vpid_b);
+	vpids.Parse();
+	return read_ok;
+}
+
+bool AJASource::ReadWireFormats(NTV2DeviceID device_id,
+				const NTV2InputSourceSet &srcs,
+				NTV2VideoFormat &vf, NTV2PixelFormat &pf,
+				VPIDDataList &vpids)
+{
+	if (srcs.empty())
+		return false;
+
+	NTV2InputSource initial_src = *srcs.begin();
+	for (auto &&src : srcs) {
+		auto channel = NTV2InputSourceToChannel(src);
+
+		mCard->EnableChannel(channel);
+		if (NTV2_INPUT_SOURCE_IS_SDI(src)) {
+			if (NTV2DeviceHasBiDirectionalSDI(device_id)) {
+				mCard->SetSDITransmitEnable(channel, false);
+				mCard->WaitForInputVerticalInterrupt(channel);
+
+				VPIDData vpid_data;
+				if (ReadChannelVPIDs(channel, vpid_data))
+					vpids.push_back(vpid_data);
+			}
+		} else if (NTV2_INPUT_SOURCE_IS_HDMI(src)) {
+			mCard->WaitForInputVerticalInterrupt(channel);
+
+			ULWord hdmi_version =
+				NTV2DeviceGetHDMIVersion(device_id);
+			// HDMIv1 handles its own RGB->YCbCr color space conversion
+			if (hdmi_version == 1) {
+				pf = kDefaultAJAPixelFormat;
+			} else {
+				NTV2LHIHDMIColorSpace hdmiInputColor;
+				mCard->GetHDMIInputColor(hdmiInputColor,
+							 channel);
+				if (hdmiInputColor ==
+				    NTV2_LHIHDMIColorSpaceYCbCr) {
+					pf = kDefaultAJAPixelFormat;
+				} else if (hdmiInputColor ==
+					   NTV2_LHIHDMIColorSpaceRGB) {
+					pf = NTV2_FBF_24BIT_BGR;
+				}
+			}
+		}
+	}
+
+	NTV2Channel initial_channel = NTV2InputSourceToChannel(initial_src);
+	mCard->WaitForInputVerticalInterrupt(initial_channel);
+
+	vf = mCard->GetInputVideoFormat(
+		initial_src, aja::Is3GLevelB(mCard, initial_channel));
+
+	if (NTV2_INPUT_SOURCE_IS_SDI(initial_src)) {
+		if (vpids.size() > 0) {
+			auto vpid = *vpids.begin();
+			if (vpid.Sampling() == VPIDSampling_YUV_422) {
+				pf = NTV2_FBF_8BIT_YCBCR;
+				blog(LOG_INFO,
+				     "AJASource::ReadWireFormats - Detected pixel format %s",
+				     NTV2FrameBufferFormatToString(pf, true)
+					     .c_str());
+			} else if (vpid.Sampling() == VPIDSampling_GBR_444) {
+				pf = NTV2_FBF_24BIT_BGR;
+				blog(LOG_INFO,
+				     "AJASource::ReadWireFormats - Detected pixel format %s",
+				     NTV2FrameBufferFormatToString(pf, true)
+					     .c_str());
+			}
+		}
+	}
+
+	blog(LOG_INFO, "AJASource::ReadWireFormats - Detected video format %s",
+	     NTV2VideoFormatToString(vf).c_str());
+
+	return true;
+}
+
+void AJASource::ResetVideoBuffer(NTV2VideoFormat vf, NTV2PixelFormat pf)
+{
+	if (vf != NTV2_FORMAT_UNKNOWN) {
+		auto videoBufferSize = GetVideoWriteSize(vf, pf);
+
+		if (mVideoBuffer)
+			mVideoBuffer.Deallocate();
+
+		mVideoBuffer.Allocate(videoBufferSize, true);
+
+		blog(LOG_INFO,
+		     "AJASource::ResetVideoBuffer: Video Format: %s | Pixel Format: %s | Buffer Size: %d",
+		     NTV2VideoFormatToString(vf, false).c_str(),
+		     NTV2FrameBufferFormatToString(pf, true).c_str(),
+		     videoBufferSize);
+	}
+}
+
+void AJASource::ResetAudioBuffer(size_t size)
+{
+	if (mAudioBuffer)
+		mAudioBuffer.Deallocate();
+	mAudioBuffer.Allocate(size, true);
+}
+
+static const char *aja_source_get_name(void *);
+static void *aja_source_create(obs_data_t *, obs_source_t *);
+static void aja_source_destroy(void *);
+static void aja_source_activate(void *);
+static void aja_source_deactivate(void *);
+static void aja_source_update(void *, obs_data_t *);
+
+const char *aja_source_get_name(void *unused)
+{
+	UNUSED_PARAMETER(unused);
+	return obs_module_text(kUIPropCaptureModule.text);
+}
+
+bool aja_source_device_changed(void *data, obs_properties_t *props,
+			       obs_property_t *list, obs_data_t *settings)
+{
+	UNUSED_PARAMETER(list);
+
+	blog(LOG_DEBUG, "AJA Source Device Changed");
+
+	auto *ajaSource = (AJASource *)data;
+	if (!ajaSource) {
+		blog(LOG_DEBUG,
+		     "aja_source_device_changed: AJA Source instance is null!");
+		return false;
+	}
+
+	auto &cardManager = aja::CardManager::Instance();
+
+	const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
+	auto cardEntry = cardManager.GetCardEntry(cardID);
+	if (!cardID || !cardEntry) {
+		blog(LOG_DEBUG,
+		     "aja_source_device_changed: Card Entry not found for %s",
+		     cardID);
+		return false;
+	}
+	blog(LOG_DEBUG, "Found CardEntry for %s", cardID);
+	CNTV2Card *card = cardEntry->GetCard();
+	if (!card) {
+		blog(LOG_DEBUG,
+		     "aja_source_device_changed: Card instance is null!");
+		return false;
+	}
+
+	const NTV2DeviceID deviceID = card->GetDeviceID();
+
+	/* If Channel 1 is actively in use, filter the video format list to only
+	 * show video formats within the same framerate family. If Channel 1 is
+	 * not active we just go ahead and try to set all framestores to the same video format.
+	 * This is because Channel 1's clock rate will govern the card's Free Run clock.
+	 */
+	NTV2VideoFormat videoFormatChannel1 = NTV2_FORMAT_UNKNOWN;
+	if (!cardEntry->ChannelReady(NTV2_CHANNEL1, ajaSource->GetName())) {
+		card->GetVideoFormat(videoFormatChannel1, NTV2_CHANNEL1);
+	}
+
+	obs_property_t *devices_list =
+		obs_properties_get(props, kUIPropDevice.id);
+	obs_property_t *io_select_list =
+		obs_properties_get(props, kUIPropInput.id);
+	obs_property_t *vid_fmt_list =
+		obs_properties_get(props, kUIPropVideoFormatSelect.id);
+	obs_property_t *pix_fmt_list =
+		obs_properties_get(props, kUIPropPixelFormatSelect.id);
+	obs_property_t *sdi_4k_list =
+		obs_properties_get(props, kUIPropSDI4KTransport.id);
+
+	obs_property_list_clear(vid_fmt_list);
+	obs_property_list_add_int(vid_fmt_list, "Auto", kVideoFormatAuto);
+	populate_video_format_list(deviceID, vid_fmt_list, videoFormatChannel1);
+
+	obs_property_list_clear(pix_fmt_list);
+	obs_property_list_add_int(pix_fmt_list, "Auto", kPixelFormatAuto);
+	populate_pixel_format_list(deviceID, pix_fmt_list);
+
+	obs_property_list_clear(sdi_4k_list);
+	populate_sdi_4k_transport_list(sdi_4k_list);
+
+	populate_io_selection_input_list(cardID, ajaSource->GetName(), deviceID,
+					 io_select_list);
+
+	auto curr_vf = static_cast<NTV2VideoFormat>(
+		obs_data_get_int(settings, kUIPropVideoFormatSelect.id));
+
+	bool have_cards = cardManager.NumCardEntries() > 0;
+	obs_property_set_visible(devices_list, have_cards);
+	obs_property_set_visible(io_select_list, have_cards);
+	obs_property_set_visible(vid_fmt_list, have_cards);
+	obs_property_set_visible(pix_fmt_list, have_cards);
+	obs_property_set_visible(
+		sdi_4k_list, have_cards && NTV2_IS_4K_VIDEO_FORMAT(curr_vf));
+
+	return true;
+}
+
+bool aja_io_selection_changed(void *data, obs_properties_t *props,
+			      obs_property_t *list, obs_data_t *settings)
+{
+	UNUSED_PARAMETER(list);
+
+	AJASource *ajaSource = (AJASource *)data;
+	if (!ajaSource) {
+		blog(LOG_DEBUG,
+		     "aja_io_selection_changed: AJA Source instance is null!");
+		return false;
+	}
+
+	auto &cardManager = aja::CardManager::Instance();
+	const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
+	auto cardEntry = cardManager.GetCardEntry(cardID);
+	if (!cardEntry) {
+		blog(LOG_DEBUG,
+		     "aja_io_selection_changed: Card Entry not found for %s",
+		     cardID);
+		return false;
+	}
+
+	obs_property_t *io_select_list =
+		obs_properties_get(props, kUIPropInput.id);
+
+	filter_io_selection_input_list(cardID, ajaSource->GetName(),
+				       io_select_list);
+
+	return true;
+}
+
+bool aja_sdi_mode_list_changed(obs_properties_t *props, obs_property_t *list,
+			       obs_data_t *settings)
+{
+	UNUSED_PARAMETER(props);
+	UNUSED_PARAMETER(list);
+	UNUSED_PARAMETER(settings);
+
+	return true;
+}
+
+void *aja_source_create(obs_data_t *settings, obs_source_t *source)
+{
+	blog(LOG_DEBUG, "AJA Source Create");
+
+	auto ajaSource = new AJASource(source);
+
+	ajaSource->SetName(obs_source_get_name(source));
+
+	obs_source_set_async_decoupled(source, true);
+
+	ajaSource->SetOBSSource(source);
+	ajaSource->ResetAudioBuffer(NTV2_AUDIOSIZE_MAX);
+	ajaSource->Activate(false);
+
+	obs_source_update(source, settings);
+
+	return ajaSource;
+}
+
+void aja_source_destroy(void *data)
+{
+	blog(LOG_DEBUG, "AJA Source Destroy");
+
+	auto ajaSource = (AJASource *)data;
+	if (!ajaSource) {
+		blog(LOG_ERROR, "aja_source_destroy: Plugin instance is null!");
+		return;
+	}
+
+	ajaSource->Deactivate();
+
+	NTV2DeviceID deviceID = DEVICE_ID_NOTFOUND;
+	CNTV2Card *card = ajaSource->GetCard();
+	if (card) {
+		deviceID = card->GetDeviceID();
+		Routing::StopSourceAudio(ajaSource->GetSourceProps(), card);
+	}
+
+	ajaSource->mVideoBuffer.Deallocate();
+	ajaSource->mAudioBuffer.Deallocate();
+	ajaSource->mVideoBuffer = NULL;
+	ajaSource->mAudioBuffer = NULL;
+
+	auto &cardManager = aja::CardManager::Instance();
+
+	const auto &cardID = ajaSource->CardID();
+
+	auto cardEntry = cardManager.GetCardEntry(cardID);
+	if (!cardEntry) {
+		blog(LOG_DEBUG,
+		     "aja_source_destroy: Card Entry not found for %s",
+		     cardID.c_str());
+		return;
+	}
+
+	auto ioSelect = ajaSource->GetSourceProps().ioSelect;
+	if (!cardEntry->ReleaseInputSelection(ioSelect, deviceID,
+					      ajaSource->GetName())) {
+		blog(LOG_WARNING,
+		     "aja_source_destroy: Error releasing Input Selection!");
+	}
+
+	delete ajaSource;
+	ajaSource = nullptr;
+}
+
+static void aja_source_show(void *data)
+{
+	auto ajaSource = (AJASource *)data;
+	if (!ajaSource) {
+		blog(LOG_ERROR,
+		     "aja_source_show: AJA Source instance is null!");
+		return;
+	}
+
+	bool deactivateWhileNotShowing =
+		ajaSource->GetSourceProps().deactivateWhileNotShowing;
+	bool showing = obs_source_showing(ajaSource->GetOBSSource());
+	blog(LOG_DEBUG,
+	     "aja_source_show: deactivateWhileNotShowing = %s, showing = %s",
+	     deactivateWhileNotShowing ? "true" : "false",
+	     showing ? "true" : "false");
+	if (deactivateWhileNotShowing && showing && !ajaSource->IsCapturing()) {
+		ajaSource->Activate(true);
+		blog(LOG_DEBUG, "aja_source_show: activated capture thread!");
+	}
+}
+
+static void aja_source_hide(void *data)
+{
+	auto ajaSource = (AJASource *)data;
+	if (!ajaSource)
+		return;
+
+	bool deactivateWhileNotShowing =
+		ajaSource->GetSourceProps().deactivateWhileNotShowing;
+	bool showing = obs_source_showing(ajaSource->GetOBSSource());
+	blog(LOG_DEBUG,
+	     "aja_source_hide: deactivateWhileNotShowing = %s, showing = %s",
+	     deactivateWhileNotShowing ? "true" : "false",
+	     showing ? "true" : "false");
+	if (deactivateWhileNotShowing && !showing && ajaSource->IsCapturing()) {
+		ajaSource->Deactivate();
+		blog(LOG_DEBUG, "aja_source_hide: deactivated capture thread!");
+	}
+}
+
+static void aja_source_activate(void *data)
+{
+	UNUSED_PARAMETER(data);
+}
+
+static void aja_source_deactivate(void *data)
+{
+	UNUSED_PARAMETER(data);
+}
+
+static void aja_source_update(void *data, obs_data_t *settings)
+{
+	blog(LOG_INFO, "aja_source_update: Begin callback...");
+
+	static bool initialized = false;
+
+	auto ajaSource = (AJASource *)data;
+	if (!ajaSource) {
+		blog(LOG_WARNING,
+		     "aja_source_update: Plugin instance is null!");
+		return;
+	}
+
+	const std::string &wantCardID =
+		obs_data_get_string(settings, kUIPropDevice.id);
+	const std::string &currentCardID = ajaSource->CardID();
+
+	auto io_select = static_cast<IOSelection>(
+		obs_data_get_int(settings, kUIPropInput.id));
+	auto vf_select = static_cast<NTV2VideoFormat>(
+		obs_data_get_int(settings, kUIPropVideoFormatSelect.id));
+	auto pf_select = static_cast<NTV2PixelFormat>(
+		obs_data_get_int(settings, kUIPropPixelFormatSelect.id));
+	auto t4_select = static_cast<SDI4KTransport>(
+		obs_data_get_int(settings, kUIPropSDI4KTransport.id));
+	bool deactivateWhileNotShowing =
+		obs_data_get_bool(settings, kUIPropDeactivateWhenNotShowing.id);
+
+	auto &cardManager = aja::CardManager::Instance();
+
+	cardManager.EnumerateCards();
+
+	if (wantCardID != currentCardID) {
+		initialized = false;
+		ajaSource->Deactivate();
+	}
+
+	auto cardEntry = cardManager.GetCardEntry(wantCardID);
+	if (!cardEntry) {
+		blog(LOG_DEBUG,
+		     "aja_source_update: Card Entry not found for %s",
+		     wantCardID.c_str());
+		return;
+	}
+
+	CNTV2Card *card = cardEntry->GetCard();
+	if (!card || !card->IsOpen()) {
+		blog(LOG_ERROR, "aja_source_update: AJA device %s not open!",
+		     wantCardID.c_str());
+		return;
+	}
+	if (card->GetModelName() == "(Not Found)") {
+		blog(LOG_ERROR,
+		     "aja_source_update: AJA device %s disconnected?",
+		     wantCardID.c_str());
+		return;
+	}
+
+	ajaSource->SetCard(cardEntry->GetCard());
+
+	SourceProps curr_props = ajaSource->GetSourceProps();
+
+	// Release Channels from previous card if card ID changes
+	if (wantCardID != currentCardID) {
+		auto prevCardEntry = cardManager.GetCardEntry(currentCardID);
+		if (prevCardEntry) {
+			const std::string &ioSelectStr =
+				aja::IOSelectionToString(curr_props.ioSelect)
+					.c_str();
+			if (!prevCardEntry->ReleaseInputSelection(
+				    curr_props.ioSelect, curr_props.deviceID,
+				    ajaSource->GetName())) {
+				blog(LOG_WARNING,
+				     "aja_source_update: Error releasing IOSelection %s for card ID %s",
+				     ioSelectStr.c_str(),
+				     currentCardID.c_str());
+			} else {
+				blog(LOG_INFO,
+				     "aja_source_update: Released IOSelection %s for card ID %s",
+				     ioSelectStr.c_str(),
+				     currentCardID.c_str());
+				ajaSource->SetCardID(wantCardID);
+				io_select = IOSelection::Invalid;
+			}
+		}
+	}
+
+	if (io_select == IOSelection::Invalid) {
+		blog(LOG_DEBUG, "aja_source_update: Invalid IOSelection");
+		return;
+	}
+
+	NTV2InputSourceSet input_srcs;
+	aja::IOSelectionToInputSources(io_select, input_srcs);
+	if (input_srcs.empty()) {
+		blog(LOG_INFO,
+		     "aja_source_update: No NTV2InputSources found for IOSelection %s",
+		     aja::IOSelectionToString(io_select).c_str());
+		return;
+	}
+
+	SourceProps want_props;
+	want_props.deviceID = card->GetDeviceID();
+	want_props.ioSelect = io_select;
+	want_props.inputSource = *input_srcs.begin();
+	want_props.videoFormat =
+		((int32_t)vf_select == kVideoFormatAuto)
+			? NTV2_FORMAT_UNKNOWN
+			: static_cast<NTV2VideoFormat>(vf_select);
+	want_props.pixelFormat =
+		((int32_t)pf_select == kPixelFormatAuto)
+			? NTV2_FBF_INVALID
+			: static_cast<NTV2PixelFormat>(pf_select);
+	want_props.sdi4kTransport = t4_select;
+	want_props.vpids.clear();
+	want_props.deactivateWhileNotShowing = deactivateWhileNotShowing;
+	want_props.autoDetect = ((int32_t)vf_select == kVideoFormatAuto ||
+				 (int32_t)pf_select == kPixelFormatAuto);
+	ajaSource->SetCardID(wantCardID);
+	ajaSource->SetDeviceIndex((UWord)cardEntry->GetCardIndex());
+
+	if (NTV2_IS_4K_VIDEO_FORMAT(want_props.videoFormat) &&
+	    want_props.sdi4kTransport == SDI4KTransport::Squares) {
+		if (want_props.ioSelect == IOSelection::SDI1_2) {
+			want_props.ioSelect = IOSelection::SDI1_2_Squares;
+		} else if (want_props.ioSelect == IOSelection::SDI3_4) {
+			want_props.ioSelect = IOSelection::SDI3_4_Squares;
+		}
+	}
+
+	// Release Channels if IOSelection changes
+	if (want_props.ioSelect != curr_props.ioSelect) {
+		const std::string &ioSelectStr =
+			aja::IOSelectionToString(curr_props.ioSelect).c_str();
+		if (!cardEntry->ReleaseInputSelection(curr_props.ioSelect,
+						      curr_props.deviceID,
+						      ajaSource->GetName())) {
+			blog(LOG_WARNING,
+			     "aja_source_update: Error releasing IOSelection %s for card ID %s",
+			     ioSelectStr.c_str(), currentCardID.c_str());
+		} else {
+			blog(LOG_INFO,
+			     "aja_source_update: Released IOSelection %s for card ID %s",
+			     ioSelectStr.c_str(), currentCardID.c_str());
+		}
+	}
+
+	// Acquire Channels for current IOSelection
+	if (!cardEntry->AcquireInputSelection(want_props.ioSelect,
+					      want_props.deviceID,
+					      ajaSource->GetName())) {
+		blog(LOG_ERROR,
+		     "aja_source_update: Could not acquire IOSelection %s",
+		     aja::IOSelectionToString(want_props.ioSelect).c_str());
+		return;
+	}
+
+	// Read SDI video payload IDs (VPID) used for helping to determine the wire format
+	NTV2VideoFormat new_vf = want_props.videoFormat;
+	NTV2PixelFormat new_pf = want_props.pixelFormat;
+	if (!ajaSource->ReadWireFormats(want_props.deviceID, input_srcs, new_vf,
+					new_pf, want_props.vpids)) {
+		blog(LOG_ERROR, "aja_source_update: ReadWireFormats failed!");
+
+		cardEntry->ReleaseInputSelection(want_props.ioSelect,
+						 curr_props.deviceID,
+						 ajaSource->GetName());
+
+		return;
+	}
+
+	// Set auto-detected formats
+	if ((int32_t)vf_select == kVideoFormatAuto)
+		want_props.videoFormat = new_vf;
+	if ((int32_t)pf_select == kPixelFormatAuto)
+		want_props.pixelFormat = new_pf;
+
+	if (want_props.videoFormat == NTV2_FORMAT_UNKNOWN ||
+	    want_props.pixelFormat == NTV2_FBF_INVALID) {
+		blog(LOG_ERROR,
+		     "aja_source_update: Unknown video/pixel format(s): %s / %s",
+		     NTV2VideoFormatToString(want_props.videoFormat).c_str(),
+		     NTV2FrameBufferFormatToString(want_props.pixelFormat)
+			     .c_str());
+
+		cardEntry->ReleaseInputSelection(want_props.ioSelect,
+						 curr_props.deviceID,
+						 ajaSource->GetName());
+
+		return;
+	}
+
+	// Change capture format and restart capture thread
+	if (!initialized || want_props != ajaSource->GetSourceProps()) {
+		Routing::ConfigureSourceRoute(want_props, NTV2_MODE_CAPTURE,
+					      card);
+
+		ajaSource->Deactivate();
+
+		initialized = true;
+	}
+
+	ajaSource->SetSourceProps(want_props);
+
+	ajaSource->ResetVideoBuffer(want_props.videoFormat,
+				    want_props.pixelFormat);
+
+	Routing::StartSourceAudio(want_props, card);
+
+	card->SetReference(NTV2_REFERENCE_FREERUN);
+
+	ajaSource->Activate(true);
+}
+
+static obs_properties_t *aja_source_get_properties(void *data)
+{
+	obs_properties_t *props = obs_properties_create();
+
+	// Uncomment below to only update when the user presses OK or Apply
+	// obs_properties_set_flags(props, OBS_PROPERTIES_DEFER_UPDATE);
+
+	obs_property_t *device_list = obs_properties_add_list(
+		props, kUIPropDevice.id, obs_module_text(kUIPropDevice.text),
+		OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
+
+	populate_source_device_list(device_list);
+
+	obs_property_t *io_select_list = obs_properties_add_list(
+		props, kUIPropInput.id, obs_module_text(kUIPropInput.text),
+		OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+
+	obs_property_t *vid_fmt_list = obs_properties_add_list(
+		props, kUIPropVideoFormatSelect.id,
+		obs_module_text(kUIPropVideoFormatSelect.text),
+		OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+
+	obs_properties_add_list(props, kUIPropPixelFormatSelect.id,
+				obs_module_text(kUIPropPixelFormatSelect.text),
+				OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+
+	obs_properties_add_list(props, kUIPropSDI4KTransport.id,
+				obs_module_text(kUIPropSDI4KTransport.text),
+				OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+
+	obs_properties_add_bool(
+		props, kUIPropDeactivateWhenNotShowing.id,
+		obs_module_text(kUIPropDeactivateWhenNotShowing.text));
+
+	obs_property_set_modified_callback(vid_fmt_list,
+					   aja_video_format_changed);
+	obs_property_set_modified_callback2(device_list,
+					    aja_source_device_changed, data);
+	obs_property_set_modified_callback2(io_select_list,
+					    aja_io_selection_changed, data);
+
+	return props;
+}
+
+void aja_source_get_defaults(obs_data_t *settings)
+{
+	obs_data_set_default_int(settings, kUIPropInput.id,
+				 static_cast<long long>(IOSelection::Invalid));
+
+	obs_data_set_default_int(settings, kUIPropVideoFormatSelect.id,
+				 static_cast<long long>(kVideoFormatAuto));
+
+	obs_data_set_default_int(
+		settings, kUIPropPixelFormatSelect.id,
+		static_cast<long long>(kDefaultAJAPixelFormat));
+
+	obs_data_set_default_int(
+		settings, kUIPropSDI4KTransport.id,
+		static_cast<long long>(SDI4KTransport::TwoSampleInterleave));
+
+	obs_data_set_default_bool(settings, kUIPropDeactivateWhenNotShowing.id,
+				  false);
+}
+
+void aja_source_save(void *data, obs_data_t *settings)
+{
+	const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
+
+	AJASource *ajaSource = (AJASource *)data;
+	if (!ajaSource) {
+		blog(LOG_ERROR,
+		     "aja_source_save: AJA Source instance is null!");
+		return;
+	}
+
+	auto &cardManager = aja::CardManager::Instance();
+	auto cardEntry = cardManager.GetCardEntry(cardID);
+	if (!cardID || !cardEntry) {
+		blog(LOG_DEBUG, "aja_source_save: Card Entry not found for %s",
+		     cardID);
+		return;
+	}
+
+	auto oldName = ajaSource->GetName();
+	auto newName = obs_source_get_name(ajaSource->GetOBSSource());
+	if (oldName != newName &&
+	    cardEntry->UpdateChannelOwnerName(oldName, newName)) {
+		ajaSource->SetName(newName);
+		blog(LOG_DEBUG, "aja_source_save: Renamed \"%s\" to \"%s\"",
+		     oldName.c_str(), newName);
+	}
+}
+
+struct obs_source_info create_aja_source_info()
+{
+	struct obs_source_info aja_source_info = {};
+	aja_source_info.id = kUIPropCaptureModule.id;
+	aja_source_info.type = OBS_SOURCE_TYPE_INPUT;
+	aja_source_info.output_flags = OBS_SOURCE_ASYNC_VIDEO |
+				       OBS_SOURCE_AUDIO |
+				       OBS_SOURCE_DO_NOT_DUPLICATE;
+	aja_source_info.get_name = aja_source_get_name;
+	aja_source_info.create = aja_source_create;
+	aja_source_info.destroy = aja_source_destroy;
+	aja_source_info.update = aja_source_update;
+	aja_source_info.show = aja_source_show;
+	aja_source_info.hide = aja_source_hide;
+	aja_source_info.activate = aja_source_activate;
+	aja_source_info.deactivate = aja_source_deactivate;
+	aja_source_info.get_properties = aja_source_get_properties;
+	aja_source_info.get_defaults = aja_source_get_defaults;
+	aja_source_info.save = aja_source_save;
+	aja_source_info.icon_type = OBS_ICON_TYPE_CAMERA;
+
+	return aja_source_info;
+}

+ 83 - 0
plugins/aja/aja-source.hpp

@@ -0,0 +1,83 @@
+#pragma once
+
+#include "aja-props.hpp"
+
+#include <obs-module.h>
+
+#include <ajantv2/includes/ajatypes.h>
+#include <ajantv2/includes/ntv2testpatterngen.h>
+
+#include <ajabase/common/types.h>
+#include <ajabase/system/thread.h>
+
+#include <mutex>
+
+class CNTV2Card;
+
+class AJASource {
+public:
+	explicit AJASource(obs_source_t *source);
+
+	~AJASource();
+
+	void SetCard(CNTV2Card *card);
+	CNTV2Card *GetCard();
+
+	void SetOBSSource(obs_source_t *source);
+	obs_source_t *GetOBSSource(void) const;
+	void SetName(const std::string &name);
+	std::string GetName() const;
+
+	void GenerateTestPattern(NTV2VideoFormat vf, NTV2PixelFormat pf,
+				 NTV2TestPatternSelect ps);
+
+	// Capture Thread stuff
+	static void CaptureThread(AJAThread *thread, void *data);
+	void Activate(bool enable = false);
+	void Deactivate();
+	bool IsCapturing() const;
+	void SetCapturing(bool capturing);
+
+	// CardEntry/Device stuff
+	std::string CardID() const;
+	void SetCardID(const std::string &cardID);
+	uint32_t DeviceIndex() const;
+	void SetDeviceIndex(uint32_t index);
+
+	// Source Props
+	void SetSourceProps(const SourceProps &props);
+	SourceProps GetSourceProps() const;
+
+	bool ReadChannelVPIDs(NTV2Channel channel, VPIDData &vpids);
+
+	bool ReadWireFormats(NTV2DeviceID device_id,
+			     const NTV2InputSourceSet &srcs,
+			     NTV2VideoFormat &vf, NTV2PixelFormat &pf,
+			     VPIDDataList &vpids);
+
+	void ResetVideoBuffer(NTV2VideoFormat vf, NTV2PixelFormat pf);
+
+	void ResetAudioBuffer(size_t size);
+
+	NTV2_POINTER mVideoBuffer;
+	NTV2_POINTER mAudioBuffer;
+
+private:
+	CNTV2Card *mCard;
+
+	std::string mSourceName;
+	std::string mCardID;
+	UWord mDeviceIndex;
+
+	bool mBuffering;
+	bool mIsCapturing;
+
+	SourceProps mSourceProps;
+
+	NTV2TestPatternBuffer mTestPattern;
+
+	AJAThread *mCaptureThread;
+	std::mutex mMutex;
+
+	obs_source_t *mSource;
+};

+ 91 - 0
plugins/aja/aja-ui-props.hpp

@@ -0,0 +1,91 @@
+#pragma once
+
+#include <obs-module.h>
+
+static const char *kProgramOutputID = "aja_output";
+static const char *kPreviewOutputID = "aja_preview_output";
+
+struct UIProperty {
+	const char *id;
+	const char *text;
+	const char *tooltip;
+};
+
+static const UIProperty kUIPropCaptureModule = {
+	"aja_source",
+	obs_module_text("AJACapture.Device"),
+	"",
+};
+
+static const UIProperty kUIPropOutputModule = {
+	"aja_output",
+	obs_module_text("AJAOutput.Device"),
+	"",
+};
+
+// This is used as an "invisible" property to give the program and preview
+// plugin instances an identifier before the output has been created/started.
+// This ID is then used by the CardManager class for tracking device channel
+// usage across the capture and output plugin instances.
+static const UIProperty kUIPropAJAOutputID = {
+	"aja_output_id",
+	"",
+	"",
+};
+
+static const UIProperty kUIPropDevice = {
+	"ui_prop_device",
+	obs_module_text("Device"),
+	"",
+};
+
+static const UIProperty kUIPropOutput = {
+	"ui_prop_output",
+	obs_module_text("Output"),
+	"",
+};
+
+static const UIProperty kUIPropInput = {
+	"ui_prop_input",
+	obs_module_text("Input"),
+	"",
+};
+
+static const UIProperty kUIPropIOSelect = {"ui_prop_select_input",
+					   obs_module_text("IOSelect"), ""};
+
+static const UIProperty kUIPropSDI4KTransport = {
+	"ui_prop_sdi_transport",
+	obs_module_text("SDI4KTransport"),
+	"",
+};
+
+static const UIProperty kUIPropVideoFormatSelect = {
+	"ui_prop_vid_fmt",
+	obs_module_text("VideoFormat"),
+	"",
+};
+
+static const UIProperty kUIPropPixelFormatSelect = {
+	"ui_prop_pix_fmt",
+	obs_module_text("PixelFormat"),
+	"",
+};
+
+static const UIProperty kUIPropAutoStartOutput = {
+	"ui_prop_auto_start_output",
+	obs_module_text("AutoStart"),
+	"",
+};
+
+static const UIProperty kUIPropDeactivateWhenNotShowing = {
+	"ui_prop_deactivate_when_not_showing",
+	obs_module_text("DeactivateWhenNotShowing"),
+	"",
+};
+
+static const UIProperty kUIPropBuffering = {
+	"ui_prop_buffering",
+	obs_module_text("Buffering"),
+	"",
+};

+ 475 - 0
plugins/aja/aja-widget-io.cpp

@@ -0,0 +1,475 @@
+#include "aja-widget-io.hpp"
+
+#include <ajantv2/includes/ntv2utils.h>
+
+#include <iostream>
+
+// firmware widget nicknames used by signal routing syntax parser
+static const char *kFramebufferNickname = "fb";
+static const char *kCSCNickname = "csc";
+static const char *kDualLinkInNickname = "dli";
+static const char *kDualLinkOutNickname = "dlo";
+static const char *kLUTNickname = "lut";
+static const char *kSDINickname = "sdi";
+static const char *kMultiLinkNickname = "ml";
+static const char *kMixerNickname = "mix";
+static const char *kHDMINickname = "hdmi";
+static const char *kLUT3DNickname = "lut3d";
+static const char *k4KDownConvertNickname = "4kdc";
+static const char *kAnalogNickname = "analog";
+static const char *kTSIMuxNickname = "tsi";
+static const char *kUpDownConvertNickname = "udc";
+static const char *kCompositeNickname = "composite";
+static const char *kStereoCompNickname = "stereo";
+static const char *kWatermarkNickname = "watermark";
+static const char *kBlackNickname = "black";
+static const char *kCompressionNickname = "comp";
+static const char *kFrameSyncNickname = "fsync";
+static const char *kTestPatternNickname = "pat";
+static const char *kOENickname = "oe";
+
+// Table of firmware widget's input crosspoint/id/channel/name/datastream index
+// clang-format off
+static const WidgetInputSocket kWidgetInputSockets[] = {
+	//NTV2InputCrosspointID        | NTV2WidgetID               | NTV2Channel           | Name | DatastreamIndex
+	{ NTV2_INPUT_CROSSPOINT_INVALID, NTV2_WIDGET_INVALID,         NTV2_CHANNEL_INVALID, "",                    -1},
+	{ NTV2_XptFrameBuffer1Input,     NTV2_WgtFrameBuffer1,        NTV2_CHANNEL1,        kFramebufferNickname,   0},
+	{ NTV2_XptFrameBuffer1BInput,    NTV2_WgtFrameBuffer1,        NTV2_CHANNEL1,        kFramebufferNickname,   1},
+	{ NTV2_XptFrameBuffer2Input,     NTV2_WgtFrameBuffer2,        NTV2_CHANNEL2,        kFramebufferNickname,   0},
+	{ NTV2_XptFrameBuffer2BInput,    NTV2_WgtFrameBuffer2,        NTV2_CHANNEL2,        kFramebufferNickname,   1},
+	{ NTV2_XptFrameBuffer3Input,     NTV2_WgtFrameBuffer3,        NTV2_CHANNEL3,        kFramebufferNickname,   0},
+	{ NTV2_XptFrameBuffer3BInput,    NTV2_WgtFrameBuffer3,        NTV2_CHANNEL3,        kFramebufferNickname,   1},
+	{ NTV2_XptFrameBuffer4Input,     NTV2_WgtFrameBuffer4,        NTV2_CHANNEL4,        kFramebufferNickname,   0},
+	{ NTV2_XptFrameBuffer4BInput,    NTV2_WgtFrameBuffer4,        NTV2_CHANNEL4,        kFramebufferNickname,   1},
+	{ NTV2_XptFrameBuffer5Input,     NTV2_WgtFrameBuffer5,        NTV2_CHANNEL5,        kFramebufferNickname,   0},
+	{ NTV2_XptFrameBuffer5BInput,    NTV2_WgtFrameBuffer5,        NTV2_CHANNEL5,        kFramebufferNickname,   1},
+	{ NTV2_XptFrameBuffer6Input,     NTV2_WgtFrameBuffer6,        NTV2_CHANNEL6,        kFramebufferNickname,   0},
+	{ NTV2_XptFrameBuffer6BInput,    NTV2_WgtFrameBuffer6,        NTV2_CHANNEL6,        kFramebufferNickname,   1},
+	{ NTV2_XptFrameBuffer7Input,     NTV2_WgtFrameBuffer7,        NTV2_CHANNEL7,        kFramebufferNickname,   0},
+	{ NTV2_XptFrameBuffer7BInput,    NTV2_WgtFrameBuffer7,        NTV2_CHANNEL7,        kFramebufferNickname,   1},
+	{ NTV2_XptFrameBuffer8Input,     NTV2_WgtFrameBuffer8,        NTV2_CHANNEL8,        kFramebufferNickname,   0},
+	{ NTV2_XptFrameBuffer8BInput,    NTV2_WgtFrameBuffer8,        NTV2_CHANNEL8,        kFramebufferNickname,   1},
+	{ NTV2_XptCSC1VidInput,          NTV2_WgtCSC1,                NTV2_CHANNEL1,        kCSCNickname,           0},
+	{ NTV2_XptCSC1KeyInput,          NTV2_WgtCSC1,                NTV2_CHANNEL1,        kCSCNickname,           1},
+	{ NTV2_XptCSC2VidInput,          NTV2_WgtCSC2,                NTV2_CHANNEL2,        kCSCNickname,           0},
+	{ NTV2_XptCSC2KeyInput,          NTV2_WgtCSC2,                NTV2_CHANNEL2,        kCSCNickname,           1},
+	{ NTV2_XptCSC3VidInput,          NTV2_WgtCSC3,                NTV2_CHANNEL3,        kCSCNickname,           0},
+	{ NTV2_XptCSC3KeyInput,          NTV2_WgtCSC3,                NTV2_CHANNEL3,        kCSCNickname,           1},
+	{ NTV2_XptCSC4VidInput,          NTV2_WgtCSC4,                NTV2_CHANNEL4,        kCSCNickname,           0},
+	{ NTV2_XptCSC4KeyInput,          NTV2_WgtCSC4,                NTV2_CHANNEL4,        kCSCNickname,           1},
+	{ NTV2_XptCSC5VidInput,          NTV2_WgtCSC5,                NTV2_CHANNEL5,        kCSCNickname,           0},
+	{ NTV2_XptCSC5KeyInput,          NTV2_WgtCSC5,                NTV2_CHANNEL5,        kCSCNickname,           1},
+	{ NTV2_XptCSC6VidInput,          NTV2_WgtCSC6,                NTV2_CHANNEL6,        kCSCNickname,           0},
+	{ NTV2_XptCSC6KeyInput,          NTV2_WgtCSC6,                NTV2_CHANNEL6,        kCSCNickname,           1},
+	{ NTV2_XptCSC7VidInput,          NTV2_WgtCSC7,                NTV2_CHANNEL7,        kCSCNickname,           0},
+	{ NTV2_XptCSC7KeyInput,          NTV2_WgtCSC7,                NTV2_CHANNEL7,        kCSCNickname,           1},
+	{ NTV2_XptCSC8VidInput,          NTV2_WgtCSC8,                NTV2_CHANNEL8,        kCSCNickname,           0},
+	{ NTV2_XptCSC8KeyInput,          NTV2_WgtCSC8,                NTV2_CHANNEL8,        kCSCNickname,           1},
+	{ NTV2_XptLUT1Input,             NTV2_WgtLUT1,                NTV2_CHANNEL1,        kLUTNickname,           0},
+	{ NTV2_XptLUT2Input,             NTV2_WgtLUT2,                NTV2_CHANNEL2,        kLUTNickname,           0},
+	{ NTV2_XptLUT3Input,             NTV2_WgtLUT3,                NTV2_CHANNEL3,        kLUTNickname,           0},
+	{ NTV2_XptLUT4Input,             NTV2_WgtLUT4,                NTV2_CHANNEL4,        kLUTNickname,           0},
+	{ NTV2_XptLUT5Input,             NTV2_WgtLUT5,                NTV2_CHANNEL5,        kLUTNickname,           0},
+	{ NTV2_XptLUT6Input,             NTV2_WgtLUT6,                NTV2_CHANNEL6,        kLUTNickname,           0},
+	{ NTV2_XptLUT7Input,             NTV2_WgtLUT7,                NTV2_CHANNEL7,        kLUTNickname,           0},
+	{ NTV2_XptLUT8Input,             NTV2_WgtLUT8,                NTV2_CHANNEL8,        kLUTNickname,           0},
+	{ NTV2_XptMultiLinkOut1Input,    NTV2_WgtMultiLinkOut1,       NTV2_CHANNEL1,        kMultiLinkNickname,     0},
+	{ NTV2_XptMultiLinkOut1InputDS2, NTV2_WgtMultiLinkOut1,       NTV2_CHANNEL1,        kMultiLinkNickname,     0},
+	{ NTV2_XptMultiLinkOut2Input,    NTV2_WgtMultiLinkOut2,       NTV2_CHANNEL1,        kMultiLinkNickname,     0},
+	{ NTV2_XptMultiLinkOut2InputDS2, NTV2_WgtMultiLinkOut2,       NTV2_CHANNEL1,        kMultiLinkNickname,     0},
+	{ NTV2_XptSDIOut1Input,          NTV2_WgtSDIOut1,             NTV2_CHANNEL1,        kSDINickname,           0},
+	{ NTV2_XptSDIOut1InputDS2,       NTV2_Wgt3GSDIOut1,           NTV2_CHANNEL1,        kSDINickname,           1},
+	{ NTV2_XptSDIOut2Input,          NTV2_WgtSDIOut2,             NTV2_CHANNEL2,        kSDINickname,           0},
+	{ NTV2_XptSDIOut2InputDS2,       NTV2_Wgt3GSDIOut2,           NTV2_CHANNEL2,        kSDINickname,           1},
+	{ NTV2_XptSDIOut3Input,          NTV2_WgtSDIOut3,             NTV2_CHANNEL3,        kSDINickname,           0},
+	{ NTV2_XptSDIOut3InputDS2,       NTV2_Wgt3GSDIOut3,           NTV2_CHANNEL3,        kSDINickname,           1},
+	{ NTV2_XptSDIOut4Input,          NTV2_WgtSDIOut4,             NTV2_CHANNEL4,        kSDINickname,           0},
+	{ NTV2_XptSDIOut4InputDS2,       NTV2_Wgt3GSDIOut4,           NTV2_CHANNEL4,        kSDINickname,           1},
+	{ NTV2_XptSDIOut5Input,          NTV2_WgtSDIMonOut1,          NTV2_CHANNEL5,        kSDINickname,           0},
+	{ NTV2_XptSDIOut5InputDS2,       NTV2_WgtSDIMonOut1,          NTV2_CHANNEL5,        kSDINickname,           1},
+	{ NTV2_XptSDIOut6Input,          NTV2_Wgt3GSDIOut6,           NTV2_CHANNEL6,        kSDINickname,           0},
+	{ NTV2_XptSDIOut6InputDS2,       NTV2_Wgt3GSDIOut6,           NTV2_CHANNEL6,        kSDINickname,           1},
+	{ NTV2_XptSDIOut7Input,          NTV2_Wgt3GSDIOut7,           NTV2_CHANNEL7,        kSDINickname,           0},
+	{ NTV2_XptSDIOut7InputDS2,       NTV2_Wgt3GSDIOut7,           NTV2_CHANNEL7,        kSDINickname,           1},
+	{ NTV2_XptSDIOut8Input,          NTV2_Wgt3GSDIOut8,           NTV2_CHANNEL8,        kSDINickname,           0},
+	{ NTV2_XptSDIOut8InputDS2,       NTV2_Wgt3GSDIOut8,           NTV2_CHANNEL8,        kSDINickname,           1},
+	{ NTV2_XptDualLinkIn1Input,      NTV2_WgtDualLinkV2In1,       NTV2_CHANNEL1,        kDualLinkInNickname,    0},
+	{ NTV2_XptDualLinkIn1DSInput,    NTV2_WgtDualLinkV2In1,       NTV2_CHANNEL1,        kDualLinkInNickname,    1},
+	{ NTV2_XptDualLinkIn2Input,      NTV2_WgtDualLinkV2In2,       NTV2_CHANNEL2,        kDualLinkInNickname,    0},
+	{ NTV2_XptDualLinkIn2DSInput,    NTV2_WgtDualLinkV2In2,       NTV2_CHANNEL2,        kDualLinkInNickname,    1},
+	{ NTV2_XptDualLinkIn3Input,      NTV2_WgtDualLinkV2In3,       NTV2_CHANNEL3,        kDualLinkInNickname,    0},
+	{ NTV2_XptDualLinkIn3DSInput,    NTV2_WgtDualLinkV2In3,       NTV2_CHANNEL3,        kDualLinkInNickname,    1},
+	{ NTV2_XptDualLinkIn4Input,      NTV2_WgtDualLinkV2In4,       NTV2_CHANNEL4,        kDualLinkInNickname,    0},
+	{ NTV2_XptDualLinkIn4DSInput,    NTV2_WgtDualLinkV2In4,       NTV2_CHANNEL4,        kDualLinkInNickname,    1},
+	{ NTV2_XptDualLinkIn5Input,      NTV2_WgtDualLinkV2In5,       NTV2_CHANNEL5,        kDualLinkInNickname,    0},
+	{ NTV2_XptDualLinkIn5DSInput,    NTV2_WgtDualLinkV2In5,       NTV2_CHANNEL5,        kDualLinkInNickname,    1},
+	{ NTV2_XptDualLinkIn6Input,      NTV2_WgtDualLinkV2In6,       NTV2_CHANNEL6,        kDualLinkInNickname,    0},
+	{ NTV2_XptDualLinkIn6DSInput,    NTV2_WgtDualLinkV2In6,       NTV2_CHANNEL6,        kDualLinkInNickname,    1},
+	{ NTV2_XptDualLinkIn7Input,      NTV2_WgtDualLinkV2In7,       NTV2_CHANNEL7,        kDualLinkInNickname,    0},
+	{ NTV2_XptDualLinkIn7DSInput,    NTV2_WgtDualLinkV2In7,       NTV2_CHANNEL7,        kDualLinkInNickname,    1},
+	{ NTV2_XptDualLinkIn8Input,      NTV2_WgtDualLinkV2In8,       NTV2_CHANNEL8,        kDualLinkInNickname,    0},
+	{ NTV2_XptDualLinkIn8DSInput,    NTV2_WgtDualLinkV2In8,       NTV2_CHANNEL8,        kDualLinkInNickname,    1},
+	{ NTV2_XptDualLinkOut1Input,     NTV2_WgtDualLinkV2Out1,      NTV2_CHANNEL1,        kDualLinkOutNickname,   0},
+	{ NTV2_XptDualLinkOut2Input,     NTV2_WgtDualLinkV2Out2,      NTV2_CHANNEL2,        kDualLinkOutNickname,   0},
+	{ NTV2_XptDualLinkOut3Input,     NTV2_WgtDualLinkV2Out3,      NTV2_CHANNEL3,        kDualLinkOutNickname,   0},
+	{ NTV2_XptDualLinkOut4Input,     NTV2_WgtDualLinkV2Out4,      NTV2_CHANNEL4,        kDualLinkOutNickname,   0},
+	{ NTV2_XptDualLinkOut5Input,     NTV2_WgtDualLinkV2Out5,      NTV2_CHANNEL5,        kDualLinkOutNickname,   0},
+	{ NTV2_XptDualLinkOut6Input,     NTV2_WgtDualLinkV2Out6,      NTV2_CHANNEL6,        kDualLinkOutNickname,   0},
+	{ NTV2_XptDualLinkOut7Input,     NTV2_WgtDualLinkV2Out7,      NTV2_CHANNEL7,        kDualLinkOutNickname,   0},
+	{ NTV2_XptDualLinkOut8Input,     NTV2_WgtDualLinkV2Out8,      NTV2_CHANNEL8,        kDualLinkOutNickname,   0},
+	{ NTV2_XptMixer1BGKeyInput,      NTV2_WgtMixer1,              NTV2_CHANNEL1,        kMixerNickname,         3},
+	{ NTV2_XptMixer1BGVidInput,      NTV2_WgtMixer1,              NTV2_CHANNEL1,        kMixerNickname,         2},
+	{ NTV2_XptMixer1FGKeyInput,      NTV2_WgtMixer1,              NTV2_CHANNEL1,        kMixerNickname,         1},
+	{ NTV2_XptMixer1FGVidInput,      NTV2_WgtMixer1,              NTV2_CHANNEL1,        kMixerNickname,         0},
+	{ NTV2_XptMixer2BGKeyInput,      NTV2_WgtMixer2,              NTV2_CHANNEL2,        kMixerNickname,         3},
+	{ NTV2_XptMixer2BGVidInput,      NTV2_WgtMixer2,              NTV2_CHANNEL2,        kMixerNickname,         2},
+	{ NTV2_XptMixer2FGKeyInput,      NTV2_WgtMixer2,              NTV2_CHANNEL2,        kMixerNickname,         1},
+	{ NTV2_XptMixer2FGVidInput,      NTV2_WgtMixer2,              NTV2_CHANNEL2,        kMixerNickname,         0},
+	{ NTV2_XptMixer3BGKeyInput,      NTV2_WgtMixer3,              NTV2_CHANNEL3,        kMixerNickname,         3},
+	{ NTV2_XptMixer3BGVidInput,      NTV2_WgtMixer3,              NTV2_CHANNEL3,        kMixerNickname,         2},
+	{ NTV2_XptMixer3FGKeyInput,      NTV2_WgtMixer3,              NTV2_CHANNEL3,        kMixerNickname,         1},
+	{ NTV2_XptMixer3FGVidInput,      NTV2_WgtMixer3,              NTV2_CHANNEL3,        kMixerNickname,         0},
+	{ NTV2_XptMixer4BGKeyInput,      NTV2_WgtMixer4,              NTV2_CHANNEL4,        kMixerNickname,         3},
+	{ NTV2_XptMixer4BGVidInput,      NTV2_WgtMixer4,              NTV2_CHANNEL4,        kMixerNickname,         2},
+	{ NTV2_XptMixer4FGKeyInput,      NTV2_WgtMixer4,              NTV2_CHANNEL4,        kMixerNickname,         1},
+	{ NTV2_XptMixer4FGVidInput,      NTV2_WgtMixer4,              NTV2_CHANNEL4,        kMixerNickname,         0},
+	{ NTV2_XptHDMIOutInput,          NTV2_WgtHDMIOut1,            NTV2_CHANNEL1,        kHDMINickname,          0},
+	{ NTV2_XptHDMIOutQ2Input,        NTV2_WgtHDMIOut1v2,          NTV2_CHANNEL1,        kHDMINickname,          1},
+	{ NTV2_XptHDMIOutQ3Input,        NTV2_WgtHDMIOut1v2,          NTV2_CHANNEL1,        kHDMINickname,          2},
+	{ NTV2_XptHDMIOutQ4Input,        NTV2_WgtHDMIOut1v2,          NTV2_CHANNEL1,        kHDMINickname,          3},
+	{ NTV2_Xpt4KDCQ1Input,           NTV2_Wgt4KDownConverter,     NTV2_CHANNEL1,        k4KDownConvertNickname, 0},
+	{ NTV2_Xpt4KDCQ2Input,           NTV2_Wgt4KDownConverter,     NTV2_CHANNEL2,        k4KDownConvertNickname, 0},
+	{ NTV2_Xpt4KDCQ3Input,           NTV2_Wgt4KDownConverter,     NTV2_CHANNEL3,        k4KDownConvertNickname, 0},
+	{ NTV2_Xpt4KDCQ4Input,           NTV2_Wgt4KDownConverter,     NTV2_CHANNEL4,        k4KDownConvertNickname, 0},
+	{ NTV2_Xpt425Mux1AInput,         NTV2_Wgt425Mux1,             NTV2_CHANNEL1,        kTSIMuxNickname,        0},
+	{ NTV2_Xpt425Mux1BInput,         NTV2_Wgt425Mux1,             NTV2_CHANNEL1,        kTSIMuxNickname,        1},
+	{ NTV2_Xpt425Mux2AInput,         NTV2_Wgt425Mux2,             NTV2_CHANNEL2,        kTSIMuxNickname,        0},
+	{ NTV2_Xpt425Mux2BInput,         NTV2_Wgt425Mux2,             NTV2_CHANNEL2,        kTSIMuxNickname,        1},
+	{ NTV2_Xpt425Mux3AInput,         NTV2_Wgt425Mux3,             NTV2_CHANNEL3,        kTSIMuxNickname,        0},
+	{ NTV2_Xpt425Mux3BInput,         NTV2_Wgt425Mux3,             NTV2_CHANNEL3,        kTSIMuxNickname,        1},
+	{ NTV2_Xpt425Mux4AInput,         NTV2_Wgt425Mux4,             NTV2_CHANNEL4,        kTSIMuxNickname,        0},
+	{ NTV2_Xpt425Mux4BInput,         NTV2_Wgt425Mux4,             NTV2_CHANNEL4,        kTSIMuxNickname,        1},
+	{ NTV2_XptAnalogOutInput,        NTV2_WgtAnalogOut1,          NTV2_CHANNEL1,        kAnalogNickname,        0},
+	{ NTV2_Xpt3DLUT1Input,           NTV2_Wgt3DLUT1,              NTV2_CHANNEL1,        kLUT3DNickname,         0},
+	{ NTV2_XptAnalogOutCompositeOut, NTV2_WgtAnalogCompositeOut1, NTV2_CHANNEL1,        kCompositeNickname,     0},
+	{ NTV2_XptStereoLeftInput,       NTV2_WgtStereoCompressor,    NTV2_CHANNEL1,        kStereoCompNickname,    0},
+	{ NTV2_XptStereoRightInput,      NTV2_WgtStereoCompressor,    NTV2_CHANNEL1,        kStereoCompNickname,    0},
+	{ NTV2_XptWaterMarker1Input,     NTV2_WgtWaterMarker1,        NTV2_CHANNEL1,        kWatermarkNickname,     0},
+	{ NTV2_XptWaterMarker2Input,     NTV2_WgtWaterMarker2,        NTV2_CHANNEL2,        kWatermarkNickname,     0},
+	{ NTV2_XptConversionMod2Input,   NTV2_WgtUpDownConverter2,    NTV2_CHANNEL2,        kUpDownConvertNickname, 0},
+	{ NTV2_XptCompressionModInput,   NTV2_WgtCompression1,        NTV2_CHANNEL1,        kCompressionNickname,   0},
+	{ NTV2_XptConversionModInput,    NTV2_WgtUpDownConverter1,    NTV2_CHANNEL1,        kUpDownConvertNickname, 0},
+	{ NTV2_XptFrameSync2Input,       NTV2_WgtFrameSync2,          NTV2_CHANNEL2,        kFrameSyncNickname,     0},
+};
+
+// Table of firmware widget's output crosspoint/id/channel/name/datastream index
+static WidgetOutputSocket kWidgetOutputSockets[] = {
+	{ NTV2_OUTPUT_CROSSPOINT_INVALID, NTV2_WIDGET_INVALID, NTV2_CHANNEL_INVALID, "", -1},
+	{ NTV2_XptBlack,                 NTV2_WgtUndefined,           NTV2_CHANNEL1,        kBlackNickname,                0},
+	{ NTV2_XptSDIIn1,                NTV2_WgtSDIIn1,              NTV2_CHANNEL1,        kSDINickname,                  0},
+	{ NTV2_XptSDIIn2,                NTV2_WgtSDIIn2,              NTV2_CHANNEL2,        kSDINickname,                  0},
+	{ NTV2_XptLUT1YUV,               NTV2_WgtLUT1,                NTV2_CHANNEL1,        kSDINickname,                  0},
+	{ NTV2_XptCSC1VidYUV,            NTV2_WgtCSC1,                NTV2_CHANNEL1,        kCSCNickname,                  0},
+	{ NTV2_XptConversionModule,      NTV2_WgtUpDownConverter1,    NTV2_CHANNEL1,        kUpDownConvertNickname,        0},
+	{ NTV2_XptCompressionModule,     NTV2_WgtCompression1,        NTV2_CHANNEL1,        kCompressionNickname,          0},
+	{ NTV2_XptFrameBuffer1YUV,       NTV2_WgtFrameBuffer1,        NTV2_CHANNEL1,        kFramebufferNickname,          0},
+	{ NTV2_XptFrameSync1YUV,         NTV2_WgtFrameSync1,          NTV2_CHANNEL1,        kFrameSyncNickname,            0},
+	{ NTV2_XptFrameSync2YUV,         NTV2_WgtFrameSync2,          NTV2_CHANNEL2,        kFrameSyncNickname,            0},
+	{ NTV2_XptDuallinkOut1,          NTV2_WgtDualLinkV2Out1,      NTV2_CHANNEL1,        kDualLinkOutNickname,          0},
+	{ NTV2_XptCSC1KeyYUV,            NTV2_WgtCSC1,                NTV2_CHANNEL1,        kCSCNickname,                  2},
+	{ NTV2_XptFrameBuffer2YUV,       NTV2_WgtFrameBuffer2,        NTV2_CHANNEL2,        kFramebufferNickname,          0},
+	{ NTV2_XptCSC2VidYUV,            NTV2_WgtCSC2,                NTV2_CHANNEL2,        kCSCNickname,                  0},
+	{ NTV2_XptCSC2KeyYUV,            NTV2_WgtCSC2,                NTV2_CHANNEL2,        kCSCNickname,                  2},
+	{ NTV2_XptMixer1VidYUV,          NTV2_WgtMixer1,              NTV2_CHANNEL1,        kMixerNickname,                0},
+	{ NTV2_XptMixer1KeyYUV,          NTV2_WgtMixer1,              NTV2_CHANNEL1,        kMixerNickname,                1},
+	{ NTV2_XptMultiLinkOut1DS1,      NTV2_WgtMultiLinkOut1,       NTV2_CHANNEL1,        kMultiLinkNickname,            0},
+	{ NTV2_XptMultiLinkOut1DS2,      NTV2_WgtMultiLinkOut1,       NTV2_CHANNEL1,        kMultiLinkNickname,            1},
+	{ NTV2_XptAnalogIn,              NTV2_WgtAnalogIn1,           NTV2_CHANNEL1,        kAnalogNickname,               0},
+	{ NTV2_XptHDMIIn1,               NTV2_WgtHDMIIn1,             NTV2_CHANNEL1,        kHDMINickname,                 0},
+	{ NTV2_XptMultiLinkOut1DS3,      NTV2_WgtMultiLinkOut1,       NTV2_CHANNEL1,        kMultiLinkNickname,            2},
+	{ NTV2_XptMultiLinkOut1DS4,      NTV2_WgtMultiLinkOut1,       NTV2_CHANNEL1,        kMultiLinkNickname,            3},
+	{ NTV2_XptMultiLinkOut2DS1,      NTV2_WgtMultiLinkOut2,       NTV2_CHANNEL2,        kMultiLinkNickname,            0},
+	{ NTV2_XptMultiLinkOut2DS2,      NTV2_WgtMultiLinkOut2,       NTV2_CHANNEL2,        kMultiLinkNickname,            1},
+	{ NTV2_XptDuallinkOut2,          NTV2_WgtDualLinkV2Out2,      NTV2_CHANNEL2,        kDualLinkOutNickname,          0},
+	{ NTV2_XptTestPatternYUV,        NTV2_WgtTestPattern1,        NTV2_CHANNEL1,        kTestPatternNickname,          0},
+	{ NTV2_XptSDIIn1DS2,             NTV2_Wgt3GSDIIn1,            NTV2_CHANNEL1,        kSDINickname,                  1},
+	{ NTV2_XptSDIIn2DS2,             NTV2_Wgt3GSDIIn2,            NTV2_CHANNEL2,        kSDINickname,                  1},
+	{ NTV2_XptMixer2VidYUV,          NTV2_WgtMixer2,              NTV2_CHANNEL2,        kMixerNickname,                0},
+	{ NTV2_XptMixer2KeyYUV,          NTV2_WgtMixer2,              NTV2_CHANNEL2,        kMixerNickname,                1},
+	{ NTV2_XptStereoCompressorOut,   NTV2_WgtStereoCompressor,    NTV2_CHANNEL1,        kStereoCompNickname,           0},
+	{ NTV2_XptFrameBuffer3YUV,       NTV2_WgtFrameBuffer3,        NTV2_CHANNEL3,        kFramebufferNickname,          0},
+	{ NTV2_XptFrameBuffer4YUV,       NTV2_WgtFrameBuffer4,        NTV2_CHANNEL4,        kFramebufferNickname,          0},
+	{ NTV2_XptDuallinkOut1DS2,       NTV2_WgtDualLinkV2Out1,      NTV2_CHANNEL1,        kDualLinkOutNickname,          1},
+	{ NTV2_XptDuallinkOut2DS2,       NTV2_WgtDualLinkV2Out2,      NTV2_CHANNEL2,        kDualLinkOutNickname,          1},
+	{ NTV2_XptCSC5VidYUV,            NTV2_WgtCSC5,                NTV2_CHANNEL5,        kCSCNickname,                  0},
+	{ NTV2_XptCSC5KeyYUV,            NTV2_WgtCSC5,                NTV2_CHANNEL5,        kCSCNickname,                  1},
+	{ NTV2_XptMultiLinkOut2DS3,      NTV2_WgtMultiLinkOut2,       NTV2_CHANNEL2,        kMultiLinkNickname,            2},
+	{ NTV2_XptMultiLinkOut2DS4,      NTV2_WgtMultiLinkOut2,       NTV2_CHANNEL2,        kMultiLinkNickname,            3},
+	{ NTV2_XptSDIIn3,                NTV2_Wgt3GSDIIn3,            NTV2_CHANNEL3,        kSDINickname,                  0},
+	{ NTV2_XptSDIIn4,                NTV2_Wgt3GSDIIn4,            NTV2_CHANNEL4,        kSDINickname,                  0},
+	{ NTV2_XptSDIIn3DS2,             NTV2_Wgt3GSDIIn3,            NTV2_CHANNEL3,        kSDINickname,                  1},
+	{ NTV2_XptSDIIn4DS2,             NTV2_Wgt3GSDIIn4,            NTV2_CHANNEL4,        kSDINickname,                  1},
+	{ NTV2_XptDuallinkOut3,          NTV2_WgtDualLinkV2Out3,      NTV2_CHANNEL3,        kDualLinkOutNickname,          0},
+	{ NTV2_XptDuallinkOut3DS2,       NTV2_WgtDualLinkV2Out3,      NTV2_CHANNEL3,        kDualLinkOutNickname,          1},
+	{ NTV2_XptDuallinkOut4,          NTV2_WgtDualLinkV2Out4,      NTV2_CHANNEL4,        kDualLinkOutNickname,          0},
+	{ NTV2_XptDuallinkOut4DS2,       NTV2_WgtDualLinkV2Out4,      NTV2_CHANNEL4,        kDualLinkOutNickname,          1},
+	{ NTV2_XptCSC3VidYUV,            NTV2_WgtCSC3,                NTV2_CHANNEL3,        kCSCNickname,                  0},
+	{ NTV2_XptCSC3KeyYUV,            NTV2_WgtCSC3,                NTV2_CHANNEL3,        kCSCNickname,                  2},
+	{ NTV2_XptCSC4VidYUV,            NTV2_WgtCSC4,                NTV2_CHANNEL4,        kCSCNickname,                  0},
+	{ NTV2_XptCSC4KeyYUV,            NTV2_WgtCSC4,                NTV2_CHANNEL4,        kCSCNickname,                  2},
+	{ NTV2_XptDuallinkOut5,          NTV2_WgtDualLinkV2Out5,      NTV2_CHANNEL5,        kDualLinkOutNickname,          0},
+	{ NTV2_XptDuallinkOut5DS2,       NTV2_WgtDualLinkV2Out5,      NTV2_CHANNEL5,        kDualLinkOutNickname,          1},
+	{ NTV2_Xpt3DLUT1YUV,             NTV2_Wgt3DLUT1,              NTV2_CHANNEL1,        kLUT3DNickname,                0},
+	{ NTV2_XptHDMIIn1Q2,             NTV2_WgtHDMIIn1v3,           NTV2_CHANNEL1,        kHDMINickname,                 1},
+	{ NTV2_XptHDMIIn1Q3,             NTV2_WgtHDMIIn1v3,           NTV2_CHANNEL1,        kHDMINickname,                 2},
+	{ NTV2_XptHDMIIn1Q4,             NTV2_WgtHDMIIn1v3,           NTV2_CHANNEL1,        kHDMINickname,                 3},
+	{ NTV2_Xpt4KDownConverterOut,    NTV2_Wgt4KDownConverter,     NTV2_CHANNEL1,        k4KDownConvertNickname,        0},
+	{ NTV2_XptSDIIn5,                NTV2_Wgt3GSDIIn5,            NTV2_CHANNEL5,        kSDINickname,                  0},
+	{ NTV2_XptSDIIn6,                NTV2_Wgt3GSDIIn6,            NTV2_CHANNEL6,        kSDINickname,                  0},
+	{ NTV2_XptSDIIn5DS2,             NTV2_Wgt3GSDIIn5,            NTV2_CHANNEL5,        kSDINickname,                  1},
+	{ NTV2_XptSDIIn6DS2,             NTV2_Wgt3GSDIIn6,            NTV2_CHANNEL6,        kSDINickname,                  1},
+	{ NTV2_XptSDIIn7,                NTV2_Wgt3GSDIIn7,            NTV2_CHANNEL7,        kSDINickname,                  0},
+	{ NTV2_XptSDIIn8,                NTV2_Wgt3GSDIIn8,            NTV2_CHANNEL8,        kSDINickname,                  0},
+	{ NTV2_XptSDIIn7DS2,             NTV2_Wgt3GSDIIn7,            NTV2_CHANNEL7,        kSDINickname,                  1},
+	{ NTV2_XptSDIIn8DS2,             NTV2_Wgt3GSDIIn8,            NTV2_CHANNEL8,        kSDINickname,                  1},
+	{ NTV2_XptFrameBuffer5YUV,       NTV2_WgtFrameBuffer5,        NTV2_CHANNEL5,        kFramebufferNickname,          0},
+	{ NTV2_XptFrameBuffer6YUV,       NTV2_WgtFrameBuffer6,        NTV2_CHANNEL6,        kFramebufferNickname,          0},
+	{ NTV2_XptFrameBuffer7YUV,       NTV2_WgtFrameBuffer7,        NTV2_CHANNEL7,        kFramebufferNickname,          0},
+	{ NTV2_XptFrameBuffer8YUV,       NTV2_WgtFrameBuffer8,        NTV2_CHANNEL8,        kFramebufferNickname,          0},
+	{ NTV2_XptMixer3VidYUV,          NTV2_WgtMixer3,              NTV2_CHANNEL3,        kMixerNickname,                0},
+	{ NTV2_XptMixer3KeyYUV,          NTV2_WgtMixer3,              NTV2_CHANNEL3,        kMixerNickname,                1},
+	{ NTV2_XptMixer4VidYUV,          NTV2_WgtMixer4,              NTV2_CHANNEL4,        kMixerNickname,                0},
+	{ NTV2_XptMixer4KeyYUV,          NTV2_WgtMixer4,              NTV2_CHANNEL4,        kMixerNickname,                1},
+	{ NTV2_XptCSC6VidYUV,            NTV2_WgtCSC6,                NTV2_CHANNEL6,        kCSCNickname,                  0},
+	{ NTV2_XptCSC6KeyYUV,            NTV2_WgtCSC6,                NTV2_CHANNEL6,        kCSCNickname,                  1},
+	{ NTV2_XptCSC7VidYUV,            NTV2_WgtCSC7,                NTV2_CHANNEL7,        kCSCNickname,                  0},
+	{ NTV2_XptCSC7KeyYUV,            NTV2_WgtCSC7,                NTV2_CHANNEL7,        kCSCNickname,                  1},
+	{ NTV2_XptCSC8VidYUV,            NTV2_WgtCSC8,                NTV2_CHANNEL8,        kCSCNickname,                  0},
+	{ NTV2_XptCSC8KeyYUV,            NTV2_WgtCSC8,                NTV2_CHANNEL8,        kCSCNickname,                  1},
+	{ NTV2_XptDuallinkOut6,          NTV2_WgtDualLinkV2Out6,      NTV2_CHANNEL6,        kDualLinkOutNickname,          0},
+	{ NTV2_XptDuallinkOut6DS2,       NTV2_WgtDualLinkV2Out6,      NTV2_CHANNEL6,        kDualLinkOutNickname,          1},
+	{ NTV2_XptDuallinkOut7,          NTV2_WgtDualLinkV2Out7,      NTV2_CHANNEL7,        kDualLinkOutNickname,          0},
+	{ NTV2_XptDuallinkOut7DS2,       NTV2_WgtDualLinkV2Out7,      NTV2_CHANNEL7,        kDualLinkOutNickname,          1},
+	{ NTV2_XptDuallinkOut8,          NTV2_WgtDualLinkV2Out8,      NTV2_CHANNEL8,        kDualLinkOutNickname,          0},
+	{ NTV2_XptDuallinkOut8DS2,       NTV2_WgtDualLinkV2Out8,      NTV2_CHANNEL8,        kDualLinkOutNickname,          1},
+	{ NTV2_Xpt425Mux1AYUV,           NTV2_Wgt425Mux1,             NTV2_CHANNEL1,        kTSIMuxNickname,               0},
+	{ NTV2_Xpt425Mux1BYUV,           NTV2_Wgt425Mux1,             NTV2_CHANNEL1,        kTSIMuxNickname,               1},
+	{ NTV2_Xpt425Mux2AYUV,           NTV2_Wgt425Mux2,             NTV2_CHANNEL2,        kTSIMuxNickname,               0},
+	{ NTV2_Xpt425Mux2BYUV,           NTV2_Wgt425Mux2,             NTV2_CHANNEL2,        kTSIMuxNickname,               1},
+	{ NTV2_Xpt425Mux3AYUV,           NTV2_Wgt425Mux3,             NTV2_CHANNEL3,        kTSIMuxNickname,               0},
+	{ NTV2_Xpt425Mux3BYUV,           NTV2_Wgt425Mux3,             NTV2_CHANNEL3,        kTSIMuxNickname,               1},
+	{ NTV2_Xpt425Mux4AYUV,           NTV2_Wgt425Mux4,             NTV2_CHANNEL4,        kTSIMuxNickname,               0},
+	{ NTV2_Xpt425Mux4BYUV,           NTV2_Wgt425Mux4,             NTV2_CHANNEL4,        kTSIMuxNickname,               1},
+	{ NTV2_XptFrameBuffer1_DS2YUV,   NTV2_WgtFrameBuffer1,        NTV2_CHANNEL1,        kFramebufferNickname,          1},
+	{ NTV2_XptFrameBuffer2_DS2YUV,   NTV2_WgtFrameBuffer2,        NTV2_CHANNEL2,        kFramebufferNickname,          1},
+	{ NTV2_XptFrameBuffer3_DS2YUV,   NTV2_WgtFrameBuffer3,        NTV2_CHANNEL3,        kFramebufferNickname,          1},
+	{ NTV2_XptFrameBuffer4_DS2YUV,   NTV2_WgtFrameBuffer4,        NTV2_CHANNEL4,        kFramebufferNickname,          1},
+	{ NTV2_XptFrameBuffer5_DS2YUV,   NTV2_WgtFrameBuffer5,        NTV2_CHANNEL5,        kFramebufferNickname,          1},
+	{ NTV2_XptFrameBuffer6_DS2YUV,   NTV2_WgtFrameBuffer6,        NTV2_CHANNEL6,        kFramebufferNickname,          1},
+	{ NTV2_XptFrameBuffer7_DS2YUV,   NTV2_WgtFrameBuffer7,        NTV2_CHANNEL7,        kFramebufferNickname,          1},
+	{ NTV2_XptFrameBuffer8_DS2YUV,   NTV2_WgtFrameBuffer8,        NTV2_CHANNEL8,        kFramebufferNickname,          1},
+	{ NTV2_XptHDMIIn2,               NTV2_WgtHDMIIn2v4,           NTV2_CHANNEL2,        kHDMINickname,                 0},
+	{ NTV2_XptHDMIIn2Q2,             NTV2_WgtHDMIIn2v4,           NTV2_CHANNEL2,        kHDMINickname,                 1},
+	{ NTV2_XptHDMIIn2Q3,             NTV2_WgtHDMIIn2v4,           NTV2_CHANNEL2,        kHDMINickname,                 2},
+	{ NTV2_XptHDMIIn2Q4,             NTV2_WgtHDMIIn2v4,           NTV2_CHANNEL2,        kHDMINickname,                 3},
+	{ NTV2_XptHDMIIn3,               NTV2_WgtHDMIIn3v4,           NTV2_CHANNEL3,        kHDMINickname,                 0},
+	{ NTV2_XptHDMIIn4,               NTV2_WgtHDMIIn4v4,           NTV2_CHANNEL4,        kHDMINickname,                 0},
+	{ NTV2_XptDuallinkIn1,           NTV2_WgtDualLinkV2In1,       NTV2_CHANNEL1,        kDualLinkInNickname,           0},
+	{ NTV2_XptLUT1Out,               NTV2_WgtLUT1,                NTV2_CHANNEL1,        kLUTNickname,                  0},
+	{ NTV2_XptCSC1VidRGB,            NTV2_WgtCSC1,                NTV2_CHANNEL1,        kCSCNickname,                  1},
+	{ NTV2_XptFrameBuffer1RGB,       NTV2_WgtFrameBuffer1,        NTV2_CHANNEL1,        kFramebufferNickname,          2},
+	{ NTV2_XptFrameSync1RGB,         NTV2_WgtFrameSync1,          NTV2_CHANNEL1,        kFrameSyncNickname,            1},
+	{ NTV2_XptFrameSync2RGB,         NTV2_WgtFrameSync2,          NTV2_CHANNEL2,        kFrameSyncNickname,            1},
+	{ NTV2_XptLUT2Out,               NTV2_WgtLUT2,                NTV2_CHANNEL2,        kLUTNickname,                  0},
+	{ NTV2_XptFrameBuffer2RGB,       NTV2_WgtFrameBuffer2,        NTV2_CHANNEL2,        kFramebufferNickname,          2},
+	{ NTV2_XptCSC2VidRGB,            NTV2_WgtCSC2,                NTV2_CHANNEL2,        kCSCNickname,                  1},
+	{ NTV2_XptMixer1VidRGB,          NTV2_WgtMixer1,              NTV2_CHANNEL1,        kMixerNickname,                1},
+	{ NTV2_XptHDMIIn1RGB,            NTV2_WgtHDMIIn1v3,           NTV2_CHANNEL1,        kHDMINickname,                 2},
+	{ NTV2_XptFrameBuffer3RGB,       NTV2_WgtFrameBuffer3,        NTV2_CHANNEL3,        kFramebufferNickname,          2},
+	{ NTV2_XptFrameBuffer4RGB,       NTV2_WgtFrameBuffer4,        NTV2_CHANNEL4,        kFramebufferNickname,          2},
+	{ NTV2_XptDuallinkIn2,           NTV2_WgtDualLinkV2In2,       NTV2_CHANNEL2,        kDualLinkInNickname,           0},
+	{ NTV2_XptLUT3Out,               NTV2_WgtLUT3,                NTV2_CHANNEL3,        kLUTNickname,                  0},
+	{ NTV2_XptLUT4Out,               NTV2_WgtLUT4,                NTV2_CHANNEL4,        kLUTNickname,                  0},
+	{ NTV2_XptLUT5Out,               NTV2_WgtLUT5,                NTV2_CHANNEL5,        kLUTNickname,                  0},
+	{ NTV2_XptCSC5VidRGB,            NTV2_WgtCSC5,                NTV2_CHANNEL5,        kCSCNickname,                  2},
+	{ NTV2_XptDuallinkIn3,           NTV2_WgtDualLinkV2In3,       NTV2_CHANNEL3,        kDualLinkInNickname,           0},
+	{ NTV2_XptDuallinkIn4,           NTV2_WgtDualLinkV2In4,       NTV2_CHANNEL4,        kDualLinkInNickname,           0},
+	{ NTV2_XptCSC3VidRGB,            NTV2_WgtCSC3,                NTV2_CHANNEL3,        kCSCNickname,                  2},
+	{ NTV2_XptCSC4VidRGB,            NTV2_WgtCSC4,                NTV2_CHANNEL4,        kCSCNickname,                  2},
+	{ NTV2_Xpt3DLUT1RGB,             NTV2_Wgt3DLUT1,              NTV2_CHANNEL1,        kLUT3DNickname,                1},
+	{ NTV2_XptHDMIIn1Q2RGB,          NTV2_WgtHDMIIn1v3,           NTV2_CHANNEL1,        kHDMINickname,                 1},
+	{ NTV2_XptHDMIIn1Q3RGB,          NTV2_WgtHDMIIn1v3,           NTV2_CHANNEL1,        kHDMINickname,                 2},
+	{ NTV2_XptHDMIIn1Q4RGB,          NTV2_WgtHDMIIn1v3,           NTV2_CHANNEL1,        kHDMINickname,                 3},
+	{ NTV2_Xpt4KDownConverterOutRGB, NTV2_Wgt4KDownConverter,     NTV2_CHANNEL1,        k4KDownConvertNickname,        1},
+	{ NTV2_XptDuallinkIn5,           NTV2_WgtDualLinkV2In5,       NTV2_CHANNEL5,        kDualLinkInNickname,           0},
+	{ NTV2_XptDuallinkIn6,           NTV2_WgtDualLinkV2In6,       NTV2_CHANNEL6,        kDualLinkInNickname,           0},
+	{ NTV2_XptDuallinkIn7,           NTV2_WgtDualLinkV2In7,       NTV2_CHANNEL7,        kDualLinkInNickname,           0},
+	{ NTV2_XptDuallinkIn8,           NTV2_WgtDualLinkV2In8,       NTV2_CHANNEL8,        kDualLinkInNickname,           0},
+	{ NTV2_XptFrameBuffer5RGB,       NTV2_WgtFrameBuffer5,        NTV2_CHANNEL5,        kFramebufferNickname,          2},
+	{ NTV2_XptFrameBuffer6RGB,       NTV2_WgtFrameBuffer6,        NTV2_CHANNEL6,        kFramebufferNickname,          2},
+	{ NTV2_XptFrameBuffer7RGB,       NTV2_WgtFrameBuffer7,        NTV2_CHANNEL7,        kFramebufferNickname,          2},
+	{ NTV2_XptFrameBuffer8RGB,       NTV2_WgtFrameBuffer8,        NTV2_CHANNEL8,        kFramebufferNickname,          2},
+	{ NTV2_XptCSC6VidRGB,            NTV2_WgtCSC6,                NTV2_CHANNEL6,        kCSCNickname,                  1},
+	{ NTV2_XptCSC7VidRGB,            NTV2_WgtCSC7,                NTV2_CHANNEL7,        kCSCNickname,                  1},
+	{ NTV2_XptCSC8VidRGB,            NTV2_WgtCSC8,                NTV2_CHANNEL8,        kCSCNickname,                  1},
+	{ NTV2_XptLUT6Out,               NTV2_WgtLUT6,                NTV2_CHANNEL6,        kLUTNickname,                  0},
+	{ NTV2_XptLUT7Out,               NTV2_WgtLUT7,                NTV2_CHANNEL7,        kLUTNickname,                  0},
+	{ NTV2_XptLUT8Out,               NTV2_WgtLUT8,                NTV2_CHANNEL8,        kLUTNickname,                  0},
+	{ NTV2_Xpt425Mux1ARGB,           NTV2_Wgt425Mux1,             NTV2_CHANNEL1,        kTSIMuxNickname,               2},
+	{ NTV2_Xpt425Mux1BRGB,           NTV2_Wgt425Mux1,             NTV2_CHANNEL1,        kTSIMuxNickname,               3},
+	{ NTV2_Xpt425Mux2ARGB,           NTV2_Wgt425Mux2,             NTV2_CHANNEL2,        kTSIMuxNickname,               2},
+	{ NTV2_Xpt425Mux2BRGB,           NTV2_Wgt425Mux2,             NTV2_CHANNEL2,        kTSIMuxNickname,               3},
+	{ NTV2_Xpt425Mux3ARGB,           NTV2_Wgt425Mux3,             NTV2_CHANNEL3,        kTSIMuxNickname,               2},
+	{ NTV2_Xpt425Mux3BRGB,           NTV2_Wgt425Mux3,             NTV2_CHANNEL3,        kTSIMuxNickname,               3},
+	{ NTV2_Xpt425Mux4ARGB,           NTV2_Wgt425Mux4,             NTV2_CHANNEL4,        kTSIMuxNickname,               2},
+	{ NTV2_Xpt425Mux4BRGB,           NTV2_Wgt425Mux4,             NTV2_CHANNEL4,        kTSIMuxNickname,               3},
+	{ NTV2_XptFrameBuffer1_DS2RGB,   NTV2_WgtFrameBuffer1,        NTV2_CHANNEL1,        kFramebufferNickname,          3},
+	{ NTV2_XptFrameBuffer2_DS2RGB,   NTV2_WgtFrameBuffer2,        NTV2_CHANNEL2,        kFramebufferNickname,          3},
+	{ NTV2_XptFrameBuffer3_DS2RGB,   NTV2_WgtFrameBuffer3,        NTV2_CHANNEL3,        kFramebufferNickname,          3},
+	{ NTV2_XptFrameBuffer4_DS2RGB,   NTV2_WgtFrameBuffer4,        NTV2_CHANNEL4,        kFramebufferNickname,          3},
+	{ NTV2_XptFrameBuffer5_DS2RGB,   NTV2_WgtFrameBuffer5,        NTV2_CHANNEL5,        kFramebufferNickname,          3},
+	{ NTV2_XptFrameBuffer6_DS2RGB,   NTV2_WgtFrameBuffer6,        NTV2_CHANNEL6,        kFramebufferNickname,          3},
+	{ NTV2_XptFrameBuffer7_DS2RGB,   NTV2_WgtFrameBuffer7,        NTV2_CHANNEL7,        kFramebufferNickname,          3},
+	{ NTV2_XptFrameBuffer8_DS2RGB,   NTV2_WgtFrameBuffer8,        NTV2_CHANNEL8,        kFramebufferNickname,          3},
+	{ NTV2_XptHDMIIn2RGB,            NTV2_WgtHDMIIn2v4,           NTV2_CHANNEL2,        kHDMINickname,                 0},
+	{ NTV2_XptHDMIIn2Q2RGB,          NTV2_WgtHDMIIn2v4,           NTV2_CHANNEL2,        kHDMINickname,                 1},
+	{ NTV2_XptHDMIIn2Q3RGB,          NTV2_WgtHDMIIn2v4,           NTV2_CHANNEL2,        kHDMINickname,                 2},
+	{ NTV2_XptHDMIIn2Q4RGB,          NTV2_WgtHDMIIn2v4,           NTV2_CHANNEL2,        kHDMINickname,                 3},
+	{ NTV2_XptHDMIIn3RGB,            NTV2_WgtHDMIIn3v4,           NTV2_CHANNEL3,        kHDMINickname,                 0},
+	{ NTV2_XptHDMIIn4RGB,            NTV2_WgtHDMIIn4v4,           NTV2_CHANNEL4,        kHDMINickname,                 0},
+};
+// clang-format on
+
+static const size_t kNumWidgetInputSockets =
+	(sizeof(kWidgetInputSockets) / sizeof(WidgetInputSocket));
+static const size_t kNumWidgetOutputSockets =
+	(sizeof(kWidgetOutputSockets) / sizeof(WidgetOutputSocket));
+
+bool WidgetInputSocket::Find(const std::string &name, NTV2Channel channel,
+			     int32_t datastream, WidgetInputSocket &inp)
+{
+	for (const auto &in : kWidgetInputSockets) {
+		if (name == in.name && channel == in.channel &&
+		    datastream == in.datastream_index) {
+			inp = in;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+bool WidgetInputSocket::GetWidgetInputSocketByXpt(InputXpt id,
+						  WidgetInputSocket &inp)
+{
+	for (const auto &in : kWidgetInputSockets) {
+		if (in.id == id) {
+			inp = in;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+int32_t WidgetInputSocket::InputXptDatastreamIndex(InputXpt xpt)
+{
+	int32_t datastream = 0;
+	for (auto &x : kWidgetInputSockets) {
+		if (x.id == xpt) {
+			datastream = x.datastream_index;
+			break;
+		}
+	}
+	return datastream;
+}
+
+NTV2Channel WidgetInputSocket::InputXptChannel(InputXpt xpt)
+{
+	NTV2Channel channel = NTV2_CHANNEL_INVALID;
+	for (auto &x : kWidgetInputSockets) {
+		if (x.id == xpt) {
+			channel = x.channel;
+			break;
+		}
+	}
+	return channel;
+}
+
+const char *WidgetInputSocket::InputXptName(InputXpt xpt)
+{
+	const char *name = NULL;
+	for (auto &x : kWidgetInputSockets) {
+		if (x.id == xpt) {
+			name = x.name;
+			break;
+		}
+	}
+	return name;
+}
+
+bool WidgetOutputSocket::Find(const std::string &name, NTV2Channel channel,
+			      int32_t datastream, WidgetOutputSocket &out)
+{
+	// std::cout << "DEBUG -- WidgetOutputSocket::Find: name = " << name
+	// 	  << ", chan = " << NTV2ChannelToString(channel)
+	// 	  << ", datastream = " << datastream << std::endl;
+	for (const auto &wo : kWidgetOutputSockets) {
+		if (name == wo.name && channel == wo.channel &&
+		    datastream == wo.datastream_index) {
+			out = wo;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+bool WidgetOutputSocket::GetWidgetOutputSocketByXpt(OutputXpt id,
+						    WidgetOutputSocket &out)
+{
+	for (const auto &wo : kWidgetOutputSockets) {
+		if (wo.id == id) {
+			out = wo;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+int32_t WidgetOutputSocket::OutputXptDatastreamIndex(OutputXpt xpt)
+{
+	int32_t datastream = 0;
+	for (auto &x : kWidgetOutputSockets) {
+		if (x.id == xpt) {
+			datastream = x.datastream_index;
+			break;
+		}
+	}
+	return datastream;
+}
+
+NTV2Channel WidgetOutputSocket::OutputXptChannel(OutputXpt xpt)
+{
+	NTV2Channel channel = NTV2_CHANNEL_INVALID;
+	for (auto &x : kWidgetOutputSockets) {
+		if (x.id == xpt) {
+			channel = x.channel;
+			break;
+		}
+	}
+	return channel;
+}
+
+const char *WidgetOutputSocket::OutputXptName(OutputXpt xpt)
+{
+	const char *name = NULL;
+	for (auto &x : kWidgetOutputSockets) {
+		if (x.id == xpt) {
+			name = x.name;
+			break;
+		}
+	}
+	return name;
+}

+ 42 - 0
plugins/aja/aja-widget-io.hpp

@@ -0,0 +1,42 @@
+#pragma once
+
+#include <ajantv2/includes/ntv2enums.h>
+
+#include <string>
+
+using OutputXpt = NTV2OutputCrosspointID; // src
+using InputXpt = NTV2InputCrosspointID;   // dest
+
+// Firmware widget input socket connector
+struct WidgetInputSocket {
+	InputXpt id;
+	NTV2WidgetID widget_id;
+	NTV2Channel channel;
+	const char *name;
+	int32_t datastream_index;
+
+	static bool Find(const std::string &route, NTV2Channel channel,
+			 int32_t datastream, WidgetInputSocket &inp);
+	static bool GetWidgetInputSocketByXpt(InputXpt id,
+					      WidgetInputSocket &inp);
+	static int32_t InputXptDatastreamIndex(InputXpt xpt);
+	static NTV2Channel InputXptChannel(InputXpt xpt);
+	static const char *InputXptName(InputXpt xpt);
+};
+
+// Firmware widget output socket connector
+struct WidgetOutputSocket {
+	OutputXpt id;
+	NTV2WidgetID widget_id;
+	NTV2Channel channel;
+	const char *name;
+	int32_t datastream_index;
+
+	static bool Find(const std::string &route, NTV2Channel channel,
+			 int32_t datastream, WidgetOutputSocket &out);
+	static bool GetWidgetOutputSocketByXpt(OutputXpt id,
+					       WidgetOutputSocket &out);
+	static int32_t OutputXptDatastreamIndex(OutputXpt xpt);
+	static NTV2Channel OutputXptChannel(OutputXpt xpt);
+	static const char *OutputXptName(OutputXpt xpt);
+};

+ 0 - 0
plugins/aja/data/.keepme


+ 15 - 0
plugins/aja/data/locale/en-US.ini

@@ -0,0 +1,15 @@
+AJACapture.Device="AJA I/O Device Capture"
+AJAOutput.Device="AJA I/O Device Output"
+Device="Device"
+Output="Output"
+Input="Input"
+Mode="Mode"
+VideoFormat="Video Format"
+PixelFormat="Pixel Format"
+AutoDetect="Auto Detect"
+Interlaced="Interlaced"
+AutoStart="Auto start on launch"
+Buffering="Use Buffering"
+DeactivateWhenNotShowing="Deactivate when not showing"
+IOSelect="Select..."
+SDI4KTransport="SDI 4K Transport"

+ 34 - 0
plugins/aja/main.cpp

@@ -0,0 +1,34 @@
+#include "aja-card-manager.hpp"
+#include <obs-module.h>
+
+OBS_DECLARE_MODULE()
+OBS_MODULE_USE_DEFAULT_LOCALE("aja", "en-US")
+
+MODULE_EXPORT const char *obs_module_description(void)
+{
+	return "aja";
+}
+
+extern struct obs_source_info create_aja_source_info();
+struct obs_source_info aja_source_info;
+
+extern struct obs_output_info create_aja_output_info();
+struct obs_output_info aja_output_info;
+
+bool obs_module_load(void)
+{
+	aja::CardManager::Instance().EnumerateCards();
+
+	aja_source_info = create_aja_source_info();
+	obs_register_source(&aja_source_info);
+
+	aja_output_info = create_aja_output_info();
+	obs_register_output(&aja_output_info);
+
+	return true;
+}
+
+void obs_module_unload(void)
+{
+	aja::CardManager::Instance().ClearCardEntries();
+}

+ 22 - 0
plugins/aja/routing/hdmi_rgb_capture.h

@@ -0,0 +1,22 @@
+#pragma once
+
+#include "../aja-routing.hpp"
+
+static const std::map<HDMIWireFormat, RoutingConfig> kHDMIRGBCaptureConfigs = {
+	{HDMIWireFormat::HD_RGB_LFR,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "hdmi[{ch1}][0]->fb[{ch1}][0];",
+	 }}};

+ 28 - 0
plugins/aja/routing/hdmi_rgb_display.h

@@ -0,0 +1,28 @@
+#pragma once
+
+#include "../aja-routing.hpp"
+
+static const std::map<HDMIWireFormat, RoutingConfig> kHDMIRGBDisplayConfigs = {
+	{HDMIWireFormat::HD_RGB_LFR,
+	 {NTV2_MODE_DISPLAY, 1, 1, true, false, false, false, false, false,
+	  false, false, false, false, "fb[{ch1}][0]->hdmi[0][0];"}},
+	{HDMIWireFormat::TTAP_PRO,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][2]->dlo[{ch1}][0];"
+		 "dlo[{ch1}][0]->sdi[{ch1}][0];"
+		 "dlo[{ch1}][1]->sdi[{ch1}][1];"
+		 "fb[{ch1}][2]->hdmi[{ch1}][0];",
+	 }}};

+ 47 - 0
plugins/aja/routing/hdmi_ycbcr_capture.h

@@ -0,0 +1,47 @@
+#pragma once
+
+#include "../aja-routing.hpp"
+
+static const std::map<HDMIWireFormat, RoutingConfig> kHDMIYCbCrCaptureConfigs = {
+	{HDMIWireFormat::HD_YCBCR_LFR,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "hdmi[{ch1}][0]->fb[{ch1}][0];",
+	 }},
+	{HDMIWireFormat::UHD_4K_YCBCR_LFR,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 2,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "hdmi[0][0]->tsi[{ch1}][0];"
+		 "hdmi[0][1]->tsi[{ch1}][1];"
+		 "hdmi[0][2]->tsi[{ch2}][0];"
+		 "hdmi[0][3]->tsi[{ch2}][1];"
+		 "tsi[{ch1}][0]->fb[{ch1}][0];"
+		 "tsi[{ch1}][1]->fb[{ch1}][1];"
+		 "tsi[{ch2}][0]->fb[{ch2}][0];"
+		 "tsi[{ch2}][1]->fb[{ch2}][1];",
+	 }},
+};

+ 64 - 0
plugins/aja/routing/hdmi_ycbcr_display.h

@@ -0,0 +1,64 @@
+#pragma once
+
+#include "../aja-routing.hpp"
+
+static const std::map<HDMIWireFormat, RoutingConfig> kHDMIYCbCrDisplayConfigs = {
+	{HDMIWireFormat::HD_YCBCR_LFR,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][0]->hdmi[0][0];",
+	 }},
+	{HDMIWireFormat::UHD_4K_YCBCR_LFR,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 2,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "fb[{ch1}][0]->tsi[{ch1}][0];"
+		 "fb[{ch1}][1]->tsi[{ch1}][1];"
+		 "fb[{ch2}][0]->tsi[{ch2}][0];"
+		 "fb[{ch2}][1]->tsi[{ch2}][1];"
+		 "tsi[{ch1}][0]->hdmi[0][0];"
+		 "tsi[{ch1}][1]->hdmi[0][1];"
+		 "tsi[{ch2}][0]->hdmi[0][2];"
+		 "tsi[{ch2}][1]->hdmi[0][3];",
+	 }},
+	{HDMIWireFormat::TTAP_PRO,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][0]->sdi[{ch1}][0];"
+		 "fb[{ch1}][0]->hdmi[{ch1}][0];",
+	 }}};

+ 466 - 0
plugins/aja/routing/sdi_rgb_capture.h

@@ -0,0 +1,466 @@
+#pragma once
+
+#include "../aja-routing.hpp"
+
+static const std::map<SDIWireFormat, RoutingConfig> kSDIRGBCaptureConfigs = {
+	{
+		SDIWireFormat::SD_ST352,
+		{
+			NTV2_MODE_CAPTURE, // i/o mode
+			1,                 // num wires
+			1,                 // num framestores
+			false,             // enable 3G output?
+			false,             // enable 6G output?
+			false,             // enable 12G output?
+			false,             // convert 3Gb -> 3Ga input?
+			false,             // convert 3Ga -> 3Gb output?
+			false,             // convert RGB 3Ga output?
+			false,             // enable 3Gb output?
+			false,             // enable 4K Square Division?
+			false,             // enable 8K Square Division?
+			false,             // enable two-sample-interleave?
+			"",
+		},
+	},
+	{SDIWireFormat::HD_720p_ST292,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->dli[{ch1}][0];"
+		 "sdi[{ch1}][1]->dli[{ch1}][1];"
+		 "dli[{ch1}][0]->fb[{ch1}][0];",
+	 }},
+	{SDIWireFormat::HD_1080_ST292,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->dli[{ch1}][0];"
+		 "sdi[{ch1}][1]->dli[{ch1}][1];"
+		 "dli[{ch1}][0]->fb[{ch1}][0];",
+	 }},
+	{SDIWireFormat::HD_1080_ST372_Dual,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->dli[{ch1}][0];"
+		 "sdi[{ch2}][0]->dli[{ch1}][1];"
+		 "dli[{ch1}][0]->fb[{ch1}][0];",
+	 }},
+	{SDIWireFormat::HD_720p_ST425_3Ga,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->dli[{ch1}][0];"
+		 "sdi[{ch1}][1]->dli[{ch1}][1];"
+		 "dli[{ch1}][0]->fb[{ch1}][0];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Ga,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->dli[{ch1}][0];"
+		 "sdi[{ch1}][1]->dli[{ch1}][1];"
+		 "dli[{ch1}][0]->fb[{ch1}][0];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Gb_DL,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->dli[{ch1}][0];"
+		 "sdi[{ch1}][1]->dli[{ch1}][1];"
+		 "dli[{ch1}][0]->fb[{ch1}][0];",
+	 }},
+	{SDIWireFormat::HD_720p_ST425_3Gb,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->dli[{ch1}][0];"
+		 "sdi[{ch1}][1]->dli[{ch1}][1];"
+		 "dli[{ch1}][0]->fb[{ch1}][0];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Gb,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->dli[{ch1}][0];"
+		 "sdi[{ch1}][1]->dli[{ch1}][1];"
+		 "dli[{ch1}][0]->fb[{ch1}][0];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_Dual_3Ga,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_Dual_3Gb,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "",
+	 }},
+	{SDIWireFormat::UHD4K_ST292_Quad_1_5_Squares,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->fb[{ch1}][0];"
+		 "sdi[{ch2}][0]->fb[{ch2}][0];"
+		 "sdi[{ch3}][0]->fb[{ch3}][0];"
+		 "sdi[{ch4}][0]->fb[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Ga_Squares,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->fb[{ch1}][0];"
+		 "sdi[{ch2}][0]->fb[{ch2}][0];"
+		 "sdi[{ch3}][0]->fb[{ch3}][0];"
+		 "sdi[{ch4}][0]->fb[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Gb_Squares,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->dli[{ch1}][0];"
+		 "sdi[{ch1}][1]->dli[{ch1}][1];"
+
+		 "sdi[{ch2}][0]->dli[{ch2}][0];"
+		 "sdi[{ch2}][1]->dli[{ch2}][1];"
+
+		 "sdi[{ch3}][0]->dli[{ch3}][0];"
+		 "sdi[{ch3}][1]->dli[{ch3}][1];"
+
+		 "sdi[{ch4}][0]->dli[{ch4}][0];"
+		 "sdi[{ch4}][1]->dli[{ch4}][1];"
+
+		 "dli[{ch1}][0]->fb[{ch1}][2];"
+		 "dli[{ch2}][0]->fb[{ch2}][2];"
+		 "dli[{ch3}][0]->fb[{ch3}][2];"
+		 "dli[{ch4}][0]->fb[{ch4}][2];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Dual_3Gb_2SI,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Ga_2SI,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 // SDI 1-4 -> Dual-Link 1-4
+		 // -> TSI Mux 1-2 -> Framestore 1-2
+		 "sdi[{ch1}][0]->dli[{ch1}][0];"
+		 "sdi[{ch1}][1]->dli[{ch1}][1];"
+		 "sdi[{ch2}][0]->dli[{ch2}][0];"
+		 "sdi[{ch2}][1]->dli[{ch2}][1];"
+		 "sdi[{ch3}][0]->dli[{ch3}][0];"
+		 "sdi[{ch3}][1]->dli[{ch3}][1];"
+		 "sdi[{ch4}][0]->dli[{ch4}][0];"
+		 "sdi[{ch4}][1]->dli[{ch4}][1];"
+
+		 "dli[{ch1}][0]->tsi[{ch1}][0];"
+		 "dli[{ch2}][0]->tsi[{ch1}][1];"
+		 "dli[{ch3}][0]->tsi[{ch2}][0];"
+		 "dli[{ch4}][0]->tsi[{ch2}][1];"
+
+		 "tsi[{ch1}][0]->fb[{ch1}][0];"
+		 "tsi[{ch1}][1]->fb[{ch1}][1];"
+		 "tsi[{ch2}][0]->fb[{ch2}][0];"
+		 "tsi[{ch2}][1]->fb[{ch2}][1];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Gb_2SI,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 // SDI 1-4 -> Dual-Link 1-4
+		 // -> TSI Mux 1-2 -> Framestore 1-2
+		 "sdi[{ch1}][0]->dli[{ch1}][0];"
+		 "sdi[{ch1}][1]->dli[{ch1}][1];"
+		 "sdi[{ch2}][0]->dli[{ch2}][0];"
+		 "sdi[{ch2}][1]->dli[{ch2}][1];"
+		 "sdi[{ch3}][0]->dli[{ch3}][0];"
+		 "sdi[{ch3}][1]->dli[{ch3}][1];"
+		 "sdi[{ch4}][0]->dli[{ch4}][0];"
+		 "sdi[{ch4}][1]->dli[{ch4}][1];"
+
+		 "dli[{ch1}][0]->tsi[{ch1}][0];"
+		 "dli[{ch2}][0]->tsi[{ch1}][1];"
+		 "dli[{ch3}][0]->tsi[{ch2}][0];"
+		 "dli[{ch4}][0]->tsi[{ch2}][1];"
+
+		 "tsi[{ch1}][0]->fb[{ch1}][0];"
+		 "tsi[{ch1}][1]->fb[{ch1}][1];"
+		 "tsi[{ch2}][0]->fb[{ch2}][0];"
+		 "tsi[{ch2}][1]->fb[{ch2}][1];",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_6G_Squares_2SI,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_12G_Squares_2SI,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_Dual_12G,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_RGB_Dual_12G,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_Quad_12G,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+};

+ 468 - 0
plugins/aja/routing/sdi_rgb_display.h

@@ -0,0 +1,468 @@
+#pragma once
+
+#include "../aja-routing.hpp"
+
+static const std::map<SDIWireFormat, RoutingConfig> kSDIRGBDisplayConfigs = {
+	{
+		SDIWireFormat::SD_ST352,
+		{
+			NTV2_MODE_DISPLAY,
+			1,     // num wires
+			1,     // num framestores
+			false, // enable 3G output?
+			false, // enable 6G output?
+			false, // enable 12G output?
+			false, // convert 3Gb -> 3Ga input?
+			false, // convert 3Ga -> 3Gb output?
+			false, // convert RGB 3Ga output?
+			false, // enable 3Gb output?
+			false, // enable 4K Square Division?
+			false, // enable 8K Square Division?
+			false, // enable two-sample-interleave?
+			"",    // RGB Output Route
+		},
+	},
+	{SDIWireFormat::HD_720p_ST292,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][2]->dlo[{ch1}][0];"
+		 "dlo[{ch1}][0]->sdi[{ch1}[0];"
+		 "dlo[{ch1}][1]->sdi[{ch1}][1];",
+	 }},
+	{SDIWireFormat::HD_1080_ST292,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][2]->dlo[{ch1}][0];"
+		 "dlo[{ch1}][0]->sdi[{ch1}[0];"
+		 "dlo[{ch1}][1]->sdi[{ch1}][1];",
+	 }},
+	{SDIWireFormat::HD_1080_ST372_Dual,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Playout
+		 "fb[{ch1}][2]->dlo[{ch1}][0];"
+		 "dlo[{ch1}][0]->sdi[{ch1}[0];"
+		 "dlo[{ch1}][1]->sdi[{ch2}][0];",
+	 }},
+	{SDIWireFormat::HD_720p_ST425_3Ga,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Output
+		 "fb[{ch1}][2]->dlo[{ch1}][0];"
+		 "dlo[{ch1}][0]->sdi[{ch1}][0];"
+		 "dlo[{ch1}][1]->sdi[{ch1}][1];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Ga,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Output
+		 "fb[{ch1}][2]->dlo[{ch1}][0];"
+		 "dlo[{ch1}][0]->sdi[{ch1}][0];"
+		 "dlo[{ch1}][1]->sdi[{ch1}][1];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Gb_DL,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Output
+		 "fb[{ch1}][2]->dlo[{ch1}][0];"
+		 "dlo[{ch1}][0]->sdi[{ch1}][0];"
+		 "dlo[{ch1}][1]->sdi[{ch1}][1];",
+	 }},
+	{SDIWireFormat::HD_720p_ST425_3Gb,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Output
+		 "fb[{ch1}][2]->dlo[{ch1}][0];"
+		 "dlo[{ch1}][0]->sdi[{ch1}][0];"
+		 "dlo[{ch1}][1]->sdi[{ch1}][1];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Gb,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 // Output
+		 "fb[{ch1}][2]->dlo[{ch1}][0];"
+		 "dlo[{ch1}][0]->sdi[{ch1}][0];"
+		 "dlo[{ch1}][1]->sdi[{ch1}][1];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_Dual_3Ga,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_Dual_3Gb,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "",
+	 }},
+	{SDIWireFormat::UHD4K_ST292_Quad_1_5_Squares,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 // Playout
+		 "fb[{ch1}][0]->sdi[{ch1}][0];"
+		 "fb[{ch2}][0]->sdi[{ch2}][0];"
+		 "fb[{ch3}][0]->sdi[{ch3}][0];"
+		 "fb[{ch4}][0]->sdi[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Ga_Squares,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 // Playout
+		 "fb[{ch1}][0]->sdi[{ch1}][0];"
+		 "fb[{ch2}][0]->sdi[{ch2}][0];"
+		 "fb[{ch3}][0]->sdi[{ch3}][0];"
+		 "fb[{ch4}][0]->sdi[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Gb_Squares,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 // Framestores 1-4 -> Dual-Link 1-4 -> SDI 1-4
+		 "fb[{ch1}][2]->dlo[{ch1}][0];"
+		 "fb[{ch2}][2]->dlo[{ch2}][0];"
+		 "fb[{ch3}][2]->dlo[{ch3}][0];"
+		 "fb[{ch4}][2]->dlo[{ch4}][0];"
+
+		 "dlo[{ch1}][0]->sdi[{ch1}][0];"
+		 "dlo[{ch1}][1]->sdi[{ch1}][1];"
+
+		 "dlo[{ch2}][0]->sdi[{ch2}][0];"
+		 "dlo[{ch2}][1]->sdi[{ch2}][1];"
+
+		 "dlo[{ch3}][0]->sdi[{ch3}][0];"
+		 "dlo[{ch3}][1]->sdi[{ch3}][1];"
+
+		 "dlo[{ch4}][0]->sdi[{ch4}][0];"
+		 "dlo[{ch4}][1]->sdi[{ch4}][1];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Dual_3Gb_2SI,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Ga_2SI,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 // Framestores 1-2 -> TSI Mux 1-2
+		 // -> Dual-Link 1-4 -> SDI 1-4
+		 "fb[{ch1}][2]->tsi[{ch1}][0];"
+		 "fb[{ch1}][3]->tsi[{ch1}][1];"
+
+		 "fb[{ch2}][2]->tsi[{ch2}][0];"
+		 "fb[{ch2}][3]->tsi[{ch2}][1];"
+
+		 "tsi[{ch1}][2]->dlo[{ch1}][0];"
+		 "tsi[{ch1}][3]->dlo[{ch2}][0];"
+		 "tsi[{ch2}][2]->dlo[{ch3}][0];"
+		 "tsi[{ch2}][3]->dlo[{ch4}][0];"
+
+		 "dlo[{ch1}][0]->sdi[{ch1}][0];"
+		 "dlo[{ch1}][1]->sdi[{ch1}][1];"
+		 "dlo[{ch2}][0]->sdi[{ch2}][0];"
+		 "dlo[{ch2}][1]->sdi[{ch2}][1];"
+		 "dlo[{ch3}][0]->sdi[{ch3}][0];"
+		 "dlo[{ch3}][1]->sdi[{ch3}][1];"
+		 "dlo[{ch4}][0]->sdi[{ch4}][0];"
+		 "dlo[{ch4}][1]->sdi[{ch4}][1];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Gb_2SI,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 // Framestores 1-2 -> TSI Mux 1-2
+		 // -> Dual-Link 1-4 -> SDI 1-4
+		 "fb[{ch1}][2]->tsi[{ch1}][0];"
+		 "fb[{ch1}][3]->tsi[{ch1}][1];"
+
+		 "fb[{ch2}][2]->tsi[{ch2}][0];"
+		 "fb[{ch2}][3]->tsi[{ch2}][1];"
+
+		 "tsi[{ch1}][2]->dlo[{ch1}][0];"
+		 "tsi[{ch1}][3]->dlo[{ch2}][0];"
+		 "tsi[{ch2}][2]->dlo[{ch3}][0];"
+		 "tsi[{ch2}][3]->dlo[{ch4}][0];"
+
+		 "dlo[{ch1}][0]->sdi[{ch1}][0];"
+		 "dlo[{ch1}][1]->sdi[{ch1}][1];"
+		 "dlo[{ch2}][0]->sdi[{ch2}][0];"
+		 "dlo[{ch2}][1]->sdi[{ch2}][1];"
+		 "dlo[{ch3}][0]->sdi[{ch3}][0];"
+		 "dlo[{ch3}][1]->sdi[{ch3}][1];"
+		 "dlo[{ch4}][0]->sdi[{ch4}][0];"
+		 "dlo[{ch4}][1]->sdi[{ch4}][1];",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_6G_Squares_2SI,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_12G_Squares_2SI,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_Dual_12G,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_RGB_Dual_12G,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_Quad_12G,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+};

+ 466 - 0
plugins/aja/routing/sdi_ycbcr_capture.h

@@ -0,0 +1,466 @@
+#pragma once
+
+#include "../aja-routing.hpp"
+
+static const std::map<SDIWireFormat, RoutingConfig> kSDIYCbCrCaptureConfigs = {
+	{
+		SDIWireFormat::SD_ST352,
+		{
+			NTV2_MODE_CAPTURE,
+			1,     // num wires
+			1,     // num framestores
+			false, // enable 3G output?
+			false, // enable 6G output?
+			false, // enable 12G output?
+			false, // convert 3Gb -> 3Ga input?
+			false, // convert 3Ga -> 3Gb output?
+			false, // convert RGB 3Ga output?
+			false, // enable 3Gb output?
+			false, // enable 4K Square Division?
+			false, // enable 8K Square Division?
+			false, // enable two-sample-interleave?
+			"sdi[{ch1}][0]->fb[{ch1}][0]", // YCbCr Capture Route
+		},
+	},
+	{SDIWireFormat::HD_720p_ST292,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->fb[{ch1}][0]",
+	 }},
+	{SDIWireFormat::HD_1080_ST292,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->fb[{ch1}][0]",
+	 }},
+	{SDIWireFormat::HD_1080_ST372_Dual,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->fb[{ch1}][0]; sdi[{ch2}][0]->fb[{ch2}][0]",
+	 }},
+	{SDIWireFormat::HD_720p_ST425_3Ga,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->fb[{ch1}][0]",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Ga,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->fb[{ch1}][0]",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Gb_DL,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 true,
+		 true,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->fb[{ch1}][0]",
+	 }},
+	{SDIWireFormat::HD_720p_ST425_3Gb,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->fb[{ch1}][0]; sdi[{ch1}][1]->fb[{ch2}][0];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Gb,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->fb[{ch1}][0]; sdi[{ch1}][1]->fb[{ch2}][0];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_Dual_3Ga,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->fb[{ch1}][0];"
+		 "sdi[{ch2}][0]->fb[{ch2}][0];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_Dual_3Gb,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->fb[{ch1}][0];"
+		 "sdi[{ch2}][0]->fb[{ch2}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST292_Dual_1_5_Squares,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 4,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 "sdi[{ch1}][0]->fb[{ch1}][0];"
+		 "sdi[{ch1}][1]->fb[{ch2}][0];"
+		 "sdi[{ch2}][0]->fb[{ch3}][0];"
+		 "sdi[{ch2}][1]->fb[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST292_Quad_1_5_Squares,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->fb[{ch1}][0];"
+		 "sdi[{ch2}][0]->fb[{ch2}][0];"
+		 "sdi[{ch3}][0]->fb[{ch3}][0];"
+		 "sdi[{ch4}][0]->fb[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Ga_Squares,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->fb[{ch1}][0];"
+		 "sdi[{ch2}][0]->fb[{ch2}][0];"
+		 "sdi[{ch3}][0]->fb[{ch3}][0];"
+		 "sdi[{ch4}][0]->fb[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Gb_Squares,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 // Capture
+		 "sdi[{ch1}][0]->fb[{ch1}][0];"
+		 "sdi[{ch2}][0]->fb[{ch2}][0];"
+		 "sdi[{ch3}][0]->fb[{ch3}][0];"
+		 "sdi[{ch4}][0]->fb[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Dual_3Gb_2SI,
+	 {NTV2_MODE_CAPTURE, 2, 2, true, false, false, false, false, false,
+	  false, false, false, true,
+	  "sdi[{ch1}][0]->tsi[{ch1}][0];"
+	  "sdi[{ch1}][1]->tsi[{ch1}][1];"
+	  "sdi[{ch2}][0]->tsi[{ch2}][0];"
+	  "sdi[{ch2}][1]->tsi[{ch2}][1];"
+	  "tsi[{ch1}][0]->fb[{ch1}][0];"
+	  "tsi[{ch1}][1]->fb[{ch1}][1];"
+	  "tsi[{ch2}][0]->fb[{ch2}][0];"
+	  "tsi[{ch2}][1]->fb[{ch2}][1];"}},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Ga_2SI,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "sdi[{ch1}][0]->tsi[{ch1}][0];"
+		 "sdi[{ch2}][0]->tsi[{ch1}][1];"
+		 "sdi[{ch3}][0]->tsi[{ch2}][0];"
+		 "sdi[{ch4}][0]->tsi[{ch2}][1];"
+		 "tsi[{ch1}][0]->fb[{ch1}][0];"
+		 "tsi[{ch1}][1]->fb[{ch1}][1];"
+		 "tsi[{ch2}][0]->fb[{ch2}][0];"
+		 "tsi[{ch2}][1]->fb[{ch2}][1];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Gb_2SI,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "sdi[{ch1}][0]->tsi[{ch1}][0];"
+		 "sdi[{ch2}][0]->tsi[{ch1}][1];"
+		 "sdi[{ch3}][0]->tsi[{ch2}][0];"
+		 "sdi[{ch4}][0]->tsi[{ch2}][1];"
+		 "tsi[{ch1}][0]->fb[{ch1}][0];"
+		 "tsi[{ch1}][1]->fb[{ch1}][1];"
+		 "tsi[{ch2}][0]->fb[{ch2}][0];"
+		 "tsi[{ch2}][1]->fb[{ch2}][1];",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_6G_Squares_2SI,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "sdi[{ch1}][0]->fb[{ch1}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_6G_Squares_2SI_Kona5_io4KPlus,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 // Capture
+		 "sdi[{ch1}][0]->tsi[{ch1}][0];"
+		 "sdi[{ch2}][0]->tsi[{ch1}][1];"
+		 "sdi[{ch3}][0]->tsi[{ch2}][0];"
+		 "sdi[{ch4}][0]->tsi[{ch2}][1];"
+		 "tsi[{ch1}][0]->fb[{ch1}][0];"
+		 "tsi[{ch1}][1]->fb[{ch1}][1];"
+		 "tsi[{ch2}][0]->fb[{ch2}][0];"
+		 "tsi[{ch2}][1]->fb[{ch2}][1];",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_12G_Squares_2SI,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "sdi[{ch1}][0]->fb[{ch1}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_12G_Squares_2SI_Kona5_io4KPlus,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 1,
+		 1,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_Dual_12G,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_RGB_Dual_12G,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 2,
+		 2,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_Quad_12G,
+	 {
+		 NTV2_MODE_CAPTURE,
+		 4,
+		 4,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+};

+ 486 - 0
plugins/aja/routing/sdi_ycbcr_display.h

@@ -0,0 +1,486 @@
+#pragma once
+
+#include "../aja-routing.hpp"
+
+static const std::map<SDIWireFormat, RoutingConfig> kSDIYCbCrDisplayConfigs = {
+	{
+		SDIWireFormat::SD_ST352,
+		{
+			NTV2_MODE_DISPLAY,
+			1,     // num wires
+			1,     // num framestores
+			false, // enable 3G output?
+			false, // enable 6G output?
+			false, // enable 12G output?
+			false, // convert 3Gb -> 3Ga input?
+			false, // convert 3Ga -> 3Gb output?
+			false, // convert RGB 3Ga output?
+			false, // enable 3Gb output?
+			false, // enable 4K Square Division?
+			false, // enable 8K Square Division?
+			false, // enable two-sample-interleave?
+			"fb[{ch1}][0]->sdi[{ch1}][0]", // YCbCr Output Route
+		},
+	},
+	{SDIWireFormat::HD_720p_ST292,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][0]->sdi[{ch1}][0]",
+	 }},
+	{SDIWireFormat::HD_1080_ST292,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][0]->sdi[{ch1}][0]",
+	 }},
+	{SDIWireFormat::HD_1080_ST372_Dual,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][0]->sdi[{ch1}][0];"
+		 "fb[{ch2}][0]->sdi[{ch2}][0]",
+	 }},
+	{SDIWireFormat::HD_720p_ST425_3Ga,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][0]->sdi[{ch1}][0]",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Ga,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][0]->sdi[{ch1}][0]",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Gb_DL,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 true,
+		 false,
+		 false,
+		 true,
+		 true,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][0]->sdi[{ch1}][0]",
+	 }},
+	{SDIWireFormat::HD_720p_ST425_3Gb,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][0]->sdi[{ch1}][0]; fb[{ch2}][0]->sdi[{ch1}][1];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_3Gb,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "fb[{ch1}][0]->sdi[{ch1}][0]; fb[{ch2}][0]->sdi[{ch1}][1];",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_Dual_3Ga,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "",
+	 }},
+	{SDIWireFormat::HD_1080p_ST425_Dual_3Gb,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 "",
+	 }},
+	{SDIWireFormat::UHD4K_ST292_Dual_1_5_Squares,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 4,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 "fb[{ch1}][0]->sdi[{ch1}][0];"
+		 "fb[{ch2}][0]->sdi[{ch1}][1];"
+		 "fb[{ch3}][0]->sdi[{ch2}][0];"
+		 "fb[{ch4}][0]->sdi[{ch2}][1];",
+	 }},
+	{SDIWireFormat::UHD4K_ST292_Quad_1_5_Squares,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 // Playout
+		 "fb[{ch1}][0]->sdi[{ch1}][0];"
+		 "fb[{ch2}][0]->sdi[{ch2}][0];"
+		 "fb[{ch3}][0]->sdi[{ch3}][0];"
+		 "fb[{ch4}][0]->sdi[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Ga_Squares,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 // Playout
+		 "fb[{ch1}][0]->sdi[{ch1}][0];"
+		 "fb[{ch2}][0]->sdi[{ch2}][0];"
+		 "fb[{ch3}][0]->sdi[{ch3}][0];"
+		 "fb[{ch4}][0]->sdi[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Gb_Squares,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 // Playout
+		 "fb[{ch1}][0]->sdi[{ch1}][0];"
+		 "fb[{ch2}][0]->sdi[{ch2}][0];"
+		 "fb[{ch3}][0]->sdi[{ch3}][0];"
+		 "fb[{ch4}][0]->sdi[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Dual_3Gb_2SI,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 true,
+		 "fb[{ch1}][0]->tsi[{ch1}][0];"
+		 "fb[{ch1}][1]->tsi[{ch1}][1];"
+		 "fb[{ch2}][0]->tsi[{ch2}][0];"
+		 "fb[{ch2}][1]->tsi[{ch2}][1];"
+		 "tsi[{ch1}][0]->sdi[{ch1}][0];"
+		 "tsi[{ch1}][1]->sdi[{ch1}][1];"
+		 "tsi[{ch2}][0]->sdi[{ch2}][0];"
+		 "tsi[{ch2}][1]->sdi[{ch2}][1];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Ga_2SI,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "fb[{ch1}][0]->tsi[{ch1}][0];"
+		 "fb[{ch1}][1]->tsi[{ch1}][1];"
+		 "fb[{ch2}][0]->tsi[{ch2}][0];"
+		 "fb[{ch2}][1]->tsi[{ch2}][1];"
+		 "tsi[{ch1}][0]->sdi[{ch1}][0];"
+		 "tsi[{ch1}][1]->sdi[{ch2}][0];"
+		 "tsi[{ch2}][0]->sdi[{ch3}][0];"
+		 "tsi[{ch2}][1]->sdi[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST425_Quad_3Gb_2SI,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 true,
+		 "fb[{ch1}][0]->tsi[{ch1}][0];"
+		 "fb[{ch1}][1]->tsi[{ch1}][1];"
+		 "fb[{ch2}][0]->tsi[{ch2}][0];"
+		 "fb[{ch2}][1]->tsi[{ch2}][1];"
+		 "tsi[{ch1}][0]->sdi[{ch1}][0];"
+		 "tsi[{ch1}][1]->sdi[{ch2}][0];"
+		 "tsi[{ch2}][0]->sdi[{ch3}][0];"
+		 "tsi[{ch2}][1]->sdi[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_6G_Squares_2SI,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "fb[{ch1}][0]->sdi[{ch1}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_6G_Squares_2SI_Kona5_io4KPlus,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 // Playout
+		 "fb[{ch3}][0]->tsi[{ch3}][0];"
+		 "fb[{ch3}][1]->tsi[{ch3}][1];"
+		 "fb[{ch4}][0]->tsi[{ch4}][0];"
+		 "fb[{ch4}][1]->tsi[{ch4}][1];"
+		 "tsi[{ch3}][0]->sdi[{ch1}][0];"
+		 "tsi[{ch3}][1]->sdi[{ch2}][0];"
+		 "tsi[{ch4}][0]->sdi[{ch3}][0];"
+		 "tsi[{ch4}][1]->sdi[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_12G_Squares_2SI,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "fb[{ch1}][0]->sdi[{ch1}][0];",
+	 }},
+	{SDIWireFormat::UHD4K_ST2018_12G_Squares_2SI_Kona5_io4KPlus,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 1,
+		 1,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 true,
+		 // Playout
+		 "fb[{ch3}][0]->tsi[{ch3}][0];"
+		 "fb[{ch3}][1]->tsi[{ch3}][1];"
+		 "fb[{ch4}][0]->tsi[{ch4}][0];"
+		 "fb[{ch4}][1]->tsi[{ch4}][1];"
+		 "tsi[{ch3}][0]->sdi[{ch1}][0];"
+		 "tsi[{ch3}][1]->sdi[{ch2}][0];"
+		 "tsi[{ch4}][0]->sdi[{ch3}][0];"
+		 "tsi[{ch4}][1]->sdi[{ch4}][0];",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_Dual_12G,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_RGB_Dual_12G,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 2,
+		 2,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+	{SDIWireFormat::UHD28K_ST2082_Quad_12G,
+	 {
+		 NTV2_MODE_DISPLAY,
+		 4,
+		 4,
+		 false,
+		 false,
+		 true,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 false,
+		 true,
+		 "",
+	 }},
+};