// Copyright (c) 2021 Michael Fabian Dirks <info@xaymar.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

//--------------------------------------------------------------------------------//
// THIS FEATURE IS DEPRECATED. SUBMITTED PATCHES WILL BE REJECTED.
//--------------------------------------------------------------------------------//

#include "encoder-aom-av1.hpp"
#include <filesystem>
#include <thread>
#include "util/util-logging.hpp"

#ifdef _DEBUG
#define ST_PREFIX "<%s> "
#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
#else
#define ST_PREFIX "<encoder::aom::av1> "
#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__)
#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__)
#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__)
#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__)
#endif

#define ST_I18N "Encoder.AOM.AV1"

// Preset
#define ST_I18N_DEPRECATED ST_I18N ".Deprecated"
#define ST_I18N_ENCODER ST_I18N ".Encoder"
#define ST_I18N_ENCODER_USAGE ST_I18N_ENCODER ".Usage"
#define ST_I18N_ENCODER_USAGE_GOODQUALITY ST_I18N_ENCODER_USAGE ".GoodQuality"
#define ST_I18N_ENCODER_USAGE_REALTIME ST_I18N_ENCODER_USAGE ".RealTime"
#define ST_I18N_ENCODER_USAGE_ALLINTRA ST_I18N_ENCODER_USAGE ".AllIntra"
#define ST_KEY_ENCODER_USAGE "Encoder.Usage"
#define ST_I18N_ENCODER_CPUUSAGE ST_I18N_ENCODER ".CPUUsage"
#define ST_I18N_ENCODER_CPUUSAGE_0 ST_I18N_ENCODER ".CPUUsage.0"
#define ST_I18N_ENCODER_CPUUSAGE_1 ST_I18N_ENCODER ".CPUUsage.1"
#define ST_I18N_ENCODER_CPUUSAGE_2 ST_I18N_ENCODER ".CPUUsage.2"
#define ST_I18N_ENCODER_CPUUSAGE_3 ST_I18N_ENCODER ".CPUUsage.3"
#define ST_I18N_ENCODER_CPUUSAGE_4 ST_I18N_ENCODER ".CPUUsage.4"
#define ST_I18N_ENCODER_CPUUSAGE_5 ST_I18N_ENCODER ".CPUUsage.5"
#define ST_I18N_ENCODER_CPUUSAGE_6 ST_I18N_ENCODER ".CPUUsage.6"
#define ST_I18N_ENCODER_CPUUSAGE_7 ST_I18N_ENCODER ".CPUUsage.7"
#define ST_I18N_ENCODER_CPUUSAGE_8 ST_I18N_ENCODER ".CPUUsage.8"
#define ST_I18N_ENCODER_CPUUSAGE_9 ST_I18N_ENCODER ".CPUUsage.9"
#define ST_I18N_ENCODER_CPUUSAGE_10 ST_I18N_ENCODER ".CPUUsage.10"
#define ST_KEY_ENCODER_CPUUSAGE "Encoder.CPUUsage"
#define ST_KEY_ENCODER_PROFILE "Encoder.Profile"

// Rate Control
#define ST_I18N_RATECONTROL ST_I18N ".RateControl"
#define ST_I18N_RATECONTROL_MODE ST_I18N_RATECONTROL ".Mode"
#define ST_I18N_RATECONTROL_MODE_CBR ST_I18N_RATECONTROL_MODE ".CBR"
#define ST_I18N_RATECONTROL_MODE_VBR ST_I18N_RATECONTROL_MODE ".VBR"
#define ST_I18N_RATECONTROL_MODE_CQ ST_I18N_RATECONTROL_MODE ".CQ"
#define ST_I18N_RATECONTROL_MODE_Q ST_I18N_RATECONTROL_MODE ".Q"
#define ST_KEY_RATECONTROL_MODE "RateControl.Mode"
#define ST_I18N_RATECONTROL_LOOKAHEAD ST_I18N_RATECONTROL ".LookAhead"
#define ST_KEY_RATECONTROL_LOOKAHEAD "RateControl.LookAhead"
#define ST_I18N_RATECONTROL_LIMITS ST_I18N_RATECONTROL ".Limits"
#define ST_I18N_RATECONTROL_LIMITS_BITRATE ST_I18N_RATECONTROL_LIMITS ".Bitrate"
#define ST_KEY_RATECONTROL_LIMITS_BITRATE "RateControl.Limits.Bitrate"
#define ST_I18N_RATECONTROL_LIMITS_BITRATE_UNDERSHOOT ST_I18N_RATECONTROL_LIMITS_BITRATE ".Undershoot"
#define ST_KEY_RATECONTROL_LIMITS_BITRATE_UNDERSHOOT "RateControl.Limits.Bitrate.Undershoot"
#define ST_I18N_RATECONTROL_LIMITS_BITRATE_OVERSHOOT ST_I18N_RATECONTROL_LIMITS_BITRATE ".Overshoot"
#define ST_KEY_RATECONTROL_LIMITS_BITRATE_OVERSHOOT "RateControl.Limits.Bitrate.Overshoot"
#define ST_I18N_RATECONTROL_LIMITS_QUALITY ST_I18N_RATECONTROL_LIMITS ".Quality"
#define ST_KEY_RATECONTROL_LIMITS_QUALITY "RateControl.Limits.Quality"
#define ST_I18N_RATECONTROL_LIMITS_QUANTIZER_MINIMUM ST_I18N_RATECONTROL_LIMITS ".Quantizer.Minimum"
#define ST_KEY_RATECONTROL_LIMITS_QUANTIZER_MINIMUM "RateControl.Limits.Quantizer.Minimum"
#define ST_I18N_RATECONTROL_LIMITS_QUANTIZER_MAXIMUM ST_I18N_RATECONTROL_LIMITS ".Quantizer.Maximum"
#define ST_KEY_RATECONTROL_LIMITS_QUANTIZER_MAXIMUM "RateControl.Limits.Quantizer.Maximum"
#define ST_I18N_RATECONTROL_BUFFER ST_I18N_RATECONTROL ".Buffer"
#define ST_I18N_RATECONTROL_BUFFER_SIZE ST_I18N_RATECONTROL_BUFFER ".Size"
#define ST_KEY_RATECONTROL_BUFFER_SIZE "RateControl.Buffer.Size"
#define ST_I18N_RATECONTROL_BUFFER_SIZE_INITIAL ST_I18N_RATECONTROL_BUFFER_SIZE ".Initial"
#define ST_KEY_RATECONTROL_BUFFER_SIZE_INITIAL "RateControl.Buffer.Size.Initial"
#define ST_I18N_RATECONTROL_BUFFER_SIZE_OPTIMAL ST_I18N_RATECONTROL_BUFFER_SIZE ".Optimal"
#define ST_KEY_RATECONTROL_BUFFER_SIZE_OPTIMAL "RateControl.Buffer.Size.Optimal"

// Key-Frames
#define ST_I18N_KEYFRAMES ST_I18N ".KeyFrames"
#define ST_I18N_KEYFRAMES_INTERVALTYPE ST_I18N_KEYFRAMES ".IntervalType"
#define ST_I18N_KEYFRAMES_INTERVALTYPE_SECONDS ST_I18N_KEYFRAMES_INTERVALTYPE ".Seconds"
#define ST_I18N_KEYFRAMES_INTERVALTYPE_FRAMES ST_I18N_KEYFRAMES_INTERVALTYPE ".Frames"
#define ST_KEY_KEYFRAMES_INTERVALTYPE "KeyFrames.IntervalType"
#define ST_I18N_KEYFRAMES_INTERVAL ST_I18N_KEYFRAMES ".Interval"
#define ST_KEY_KEYFRAMES_INTERVAL_SECONDS "KeyFrames.Interval.Seconds"
#define ST_KEY_KEYFRAMES_INTERVAL_FRAMES "KeyFrames.Interval.Frames"

// Advanced
#define ST_I18N_ADVANCED ST_I18N ".Advanced"
#define ST_I18N_ADVANCED_THREADS ST_I18N_ADVANCED ".Threads"
#define ST_KEY_ADVANCED_THREADS "Advanced.Threads"
#define ST_I18N_ADVANCED_ROWMULTITHREADING ST_I18N_ADVANCED ".RowMultiThreading"
#define ST_KEY_ADVANCED_ROWMULTITHREADING "Advanced.RowMultiThreading"
#define ST_I18N_ADVANCED_TILE_COLUMNS ST_I18N_ADVANCED ".Tile.Columns"
#define ST_KEY_ADVANCED_TILE_COLUMNS "Advanced.Tile.Columns"
#define ST_I18N_ADVANCED_TILE_ROWS ST_I18N_ADVANCED ".Tile.Rows"
#define ST_KEY_ADVANCED_TILE_ROWS "Advanced.Tile.Rows"
#define ST_I18N_ADVANCED_TUNE ST_I18N_ADVANCED ".Tune"
#define ST_I18N_ADVANCED_TUNE_METRIC ST_I18N_ADVANCED_TUNE ".Metric"
#define ST_I18N_ADVANCED_TUNE_METRIC_PSNR ST_I18N_ADVANCED_TUNE_METRIC ".PSNR"
#define ST_I18N_ADVANCED_TUNE_METRIC_SSIM ST_I18N_ADVANCED_TUNE_METRIC ".SSIM"
#define ST_I18N_ADVANCED_TUNE_METRIC_VMAFWITHPREPROCESSING ST_I18N_ADVANCED_TUNE_METRIC ".VMAF.WithPreprocessing"
#define ST_I18N_ADVANCED_TUNE_METRIC_VMAFWITHOUTPREPROCESSING ST_I18N_ADVANCED_TUNE_METRIC ".VMAF.WithoutPreProcessing"
#define ST_I18N_ADVANCED_TUNE_METRIC_VMAFMAXGAIN ST_I18N_ADVANCED_TUNE_METRIC ".VMAF.MaxGain"
#define ST_I18N_ADVANCED_TUNE_METRIC_VMAFNEGMAXGAIN ST_I18N_ADVANCED_TUNE_METRIC ".VMAF.NegMaxGain"
#define ST_I18N_ADVANCED_TUNE_METRIC_BUTTERAUGLI ST_I18N_ADVANCED_TUNE_METRIC ".Butteraugli"
#define ST_KEY_ADVANCED_TUNE_METRIC "Advanced.Tune.Metric"
#define ST_I18N_ADVANCED_TUNE_CONTENT ST_I18N_ADVANCED_TUNE ".Content"
#define ST_I18N_ADVANCED_TUNE_CONTENT_SCREEN ST_I18N_ADVANCED_TUNE_CONTENT ".Screen"
#define ST_I18N_ADVANCED_TUNE_CONTENT_FILM ST_I18N_ADVANCED_TUNE_CONTENT ".Film"
#define ST_KEY_ADVANCED_TUNE_CONTENT "Advanced.Tune.Content"

using namespace streamfx::encoder::aom::av1;

static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Encoder-AOM-AV1";

const char* obs_video_format_to_string(video_format format)
{
	switch (format) {
	case VIDEO_FORMAT_I420:
		return "I420";
	case VIDEO_FORMAT_NV12:
		return "NV12";
	case VIDEO_FORMAT_YVYU:
		return "YVYU";
	case VIDEO_FORMAT_YUY2:
		return "YUY";
	case VIDEO_FORMAT_UYVY:
		return "UYVY";
	case VIDEO_FORMAT_RGBA:
		return "RGBA";
	case VIDEO_FORMAT_BGRA:
		return "BGRA";
	case VIDEO_FORMAT_BGRX:
		return "BGRX";
	case VIDEO_FORMAT_Y800:
		return "Y800";
	case VIDEO_FORMAT_I444:
		return "I444";
	case VIDEO_FORMAT_BGR3:
		return "BGR3";
	case VIDEO_FORMAT_I422:
		return "I422";
	case VIDEO_FORMAT_I40A:
		return "I40A";
	case VIDEO_FORMAT_I42A:
		return "I42A";
	case VIDEO_FORMAT_YUVA:
		return "YUVA";
	case VIDEO_FORMAT_AYUV:
		return "AYUV";
	default:
		return "Unknown";
	}
}

const char* aom_color_format_to_string(aom_img_fmt format)
{
	switch (format) {
	case AOM_IMG_FMT_AOMYV12:
		return "AOM-YV12";
	case AOM_IMG_FMT_AOMI420:
		return "AOM-I420";
	case AOM_IMG_FMT_YV12:
		return "YV12";
	case AOM_IMG_FMT_I420:
		return "I420";
	case AOM_IMG_FMT_I422:
		return "I422";
	case AOM_IMG_FMT_I444:
		return "I444";
	case AOM_IMG_FMT_YV1216:
		return "YV12-16";
	case AOM_IMG_FMT_I42016:
		return "I420-16";
	case AOM_IMG_FMT_I42216:
		return "I422-16";
	case AOM_IMG_FMT_I44416:
		return "I444-16";
	default:
		return "Unknown";
	}
}

const char* aom_color_trc_to_string(aom_transfer_characteristics_t trc)
{
	switch (trc) {
	case AOM_CICP_TC_BT_709:
		return "Bt.709";
	case AOM_CICP_TC_BT_470_M:
		return "Bt.407 M";
	case AOM_CICP_TC_BT_470_B_G:
		return "Bt.407 B/G";
	case AOM_CICP_TC_BT_601:
		return "Bt.601";
	case AOM_CICP_TC_SMPTE_240:
		return "SMPTE 240 M";
	case AOM_CICP_TC_LINEAR:
		return "Linear";
	case AOM_CICP_TC_LOG_100:
		return "Logarithmic (100:1 range)";
	case AOM_CICP_TC_LOG_100_SQRT10:
		return "Logarithmic (100*sqrt(10):1 range)";
	case AOM_CICP_TC_IEC_61966:
		return "IEC 61966-2-4";
	case AOM_CICP_TC_BT_1361:
		return "Bt.1361";
	case AOM_CICP_TC_SRGB:
		return "sRGB";
	case AOM_CICP_TC_BT_2020_10_BIT:
		return "Bt.2020 10b";
	case AOM_CICP_TC_BT_2020_12_BIT:
		return "Bt.2020 12b";
	case AOM_CICP_TC_SMPTE_2084:
		return "Bt.2100 PQ";
	case AOM_CICP_TC_SMPTE_428:
		return "SMPTE ST 428";
	case AOM_CICP_TC_HLG:
		return "Bt.2100 HLG";
	default:
		return "Unknown";
	}
}

const char* aom_rc_mode_to_string(aom_rc_mode mode)
{
	switch (mode) {
	case AOM_VBR:
		return "Variable Bitrate (VBR)";
	case AOM_CBR:
		return "Constant Bitrate (CBR)";
	case AOM_CQ:
		return "Constrained Quality (CQ)";
	case AOM_Q:
		return "Constant Quality (Q)";
	default:
		return "Unknown";
	}
}

const char* aom_kf_mode_to_string(aom_kf_mode mode)
{
	switch (mode) {
	case AOM_KF_AUTO:
		return "Automatic";
	case AOM_KF_DISABLED:
		return "Disabled";
	default:
		return "Unknown";
	}
}

const char* aom_tune_metric_to_string(aom_tune_metric tune)
{
	switch (tune) {
	case AOM_TUNE_PSNR:
		return "PSNR";
	case AOM_TUNE_SSIM:
		return "SSIM";
	case AOM_TUNE_VMAF_WITH_PREPROCESSING:
		return "VMAF w/ pre-processing";
	case AOM_TUNE_VMAF_WITHOUT_PREPROCESSING:
		return "VMAF w/o pre-processing";
	case AOM_TUNE_VMAF_MAX_GAIN:
		return "VMAF max. gain";
	case AOM_TUNE_VMAF_NEG_MAX_GAIN:
		return "VMAF negative max. gain";
	case AOM_TUNE_BUTTERAUGLI:
		return "Butteraugli";
	default:
		return "Unknown";
	}
}

const char* aom_tune_content_to_string(aom_tune_content tune)
{
	switch (tune) {
	case AOM_CONTENT_DEFAULT:
		return "Default";
	case AOM_CONTENT_FILM:
		return "Film";
	case AOM_CONTENT_SCREEN:
		return "Screen";
	default:
		return "Unknown";
	}
}

aom_av1_instance::aom_av1_instance(obs_data_t* settings, obs_encoder_t* self, bool is_hw)
	: obs::encoder_instance(settings, self, is_hw), _factory(aom_av1_factory::get()), _iface(nullptr), _ctx(), _cfg(),
	  _image_index(0), _images(), _global_headers(nullptr), _initialized(false), _settings()
{
	if (is_hw) {
		throw std::runtime_error("Hardware encoding isn't even registered, how did you get here?");
	}

	// Retrieve encoder interface.
	_iface = _factory->libaom_codec_av1_cx();
	if (!_iface) {
		throw std::runtime_error("AOM library does not provide AV1 encoder.");
	}

#ifdef ENABLE_PROFILING
	// Profilers
	_profiler_copy   = streamfx::util::profiler::create();
	_profiler_encode = streamfx::util::profiler::create();
	_profiler_packet = streamfx::util::profiler::create();
#endif

	{     // Generate Static Configuration
		{ // OBS Information
			video_scale_info                ovsi;
			video_t*                        video      = obs_encoder_video(_self);
			const struct video_output_info* video_info = video_output_get_info(video);

			ovsi.colorspace = video_info->colorspace;
			ovsi.format     = video_info->format;
			ovsi.range      = video_info->range;
			get_video_info(&ovsi);

			// Video
			_settings.width   = static_cast<uint16_t>(obs_encoder_get_width(_self));
			_settings.height  = static_cast<uint16_t>(obs_encoder_get_height(_self));
			_settings.fps.num = static_cast<uint32_t>(video_info->fps_num);
			_settings.fps.den = static_cast<uint32_t>(video_info->fps_den);

			// Color Format
			switch (ovsi.format) {
			case VIDEO_FORMAT_I420:
				_settings.color_format = AOM_IMG_FMT_I420;
				break;
			case VIDEO_FORMAT_I422:
				_settings.color_format = AOM_IMG_FMT_I422;
				break;
			case VIDEO_FORMAT_I444:
				_settings.color_format = AOM_IMG_FMT_I444;
				break;
			default:
				throw std::runtime_error("Something went wrong figuring out our color format.");
			}

			// Color Space
			switch (ovsi.colorspace) {
			case VIDEO_CS_601:
				_settings.color_primaries = AOM_CICP_CP_BT_601;
				_settings.color_trc       = AOM_CICP_TC_BT_601;
				_settings.color_matrix    = AOM_CICP_MC_BT_601;
				break;
			case VIDEO_CS_709:
				_settings.color_primaries = AOM_CICP_CP_BT_709;
				_settings.color_trc       = AOM_CICP_TC_BT_709;
				_settings.color_matrix    = AOM_CICP_MC_BT_709;
				break;
			case VIDEO_CS_SRGB:
				_settings.color_primaries = AOM_CICP_CP_BT_709;
				_settings.color_trc       = AOM_CICP_TC_SRGB;
				_settings.color_matrix    = AOM_CICP_MC_BT_709;
				break;
			}

			// Color Range
			switch (ovsi.range) {
			case VIDEO_RANGE_FULL:
				_settings.color_range = AOM_CR_FULL_RANGE;
				break;
			case VIDEO_RANGE_PARTIAL:
				_settings.color_range = AOM_CR_STUDIO_RANGE;
				break;
			}

			// Monochrome
			_settings.monochrome = (video_info->format == VIDEO_FORMAT_Y800);
		}

		{ // Encoder
			_settings.profile = static_cast<codec::av1::profile>(obs_data_get_int(settings, ST_KEY_ENCODER_PROFILE));
			if (_settings.profile == codec::av1::profile::UNKNOWN) {
				// Resolve the automatic profile to a proper value.
				bool need_professional = (_settings.color_format == AOM_IMG_FMT_I422);
				bool need_high         = (_settings.color_format == AOM_IMG_FMT_I444) || _settings.monochrome;

				if (need_professional) {
					_settings.profile = codec::av1::profile::PROFESSIONAL;
				} else if (need_high) {
					_settings.profile = codec::av1::profile::HIGH;
				} else {
					_settings.profile = codec::av1::profile::MAIN;
				}
			}
		}

		{ // Rate Control
			_settings.rc_mode      = static_cast<aom_rc_mode>(obs_data_get_int(settings, ST_KEY_RATECONTROL_MODE));
			_settings.rc_lookahead = static_cast<int8_t>(obs_data_get_int(settings, ST_KEY_RATECONTROL_LOOKAHEAD));
		}

		{ // Threading
			if (auto threads = obs_data_get_int(settings, ST_KEY_ADVANCED_THREADS); threads > 0) {
				_settings.threads = static_cast<int8_t>(threads);
			} else {
				_settings.threads = std::thread::hardware_concurrency();
			}
			_settings.rowmultithreading =
				static_cast<int8_t>(obs_data_get_int(settings, ST_KEY_ADVANCED_ROWMULTITHREADING));
		}

		{ // Tiling
			_settings.tile_columns = static_cast<int8_t>(obs_data_get_int(settings, ST_KEY_ADVANCED_TILE_COLUMNS));
			_settings.tile_rows    = static_cast<int8_t>(obs_data_get_int(settings, ST_KEY_ADVANCED_TILE_ROWS));
		}

		{ // Tuning
			if (auto v = obs_data_get_int(settings, ST_KEY_ADVANCED_TUNE_METRIC); v != -1) {
				_settings.tune_metric = static_cast<aom_tune_metric>(v);
			}
			_settings.tune_content =
				static_cast<aom_tune_content>(obs_data_get_int(settings, ST_KEY_ADVANCED_TUNE_CONTENT));
		}
	}

	// Apply Settings
	update(settings);

	// Initialize Encoder
	if (auto error = _factory->libaom_codec_enc_init_ver(&_ctx, _iface, &_cfg, 0, AOM_ENCODER_ABI_VERSION);
		error != AOM_CODEC_OK) {
		const char* errstr = _factory->libaom_codec_err_to_string(error);
		D_LOG_ERROR("Failed to initialize codec, unexpected error: %s (code %" PRIu32 ")", errstr, error);
		throw std::runtime_error(errstr);
	}

	{ // Apply Static Control Settings

		{ // Color Information
#ifdef AOM_CTRL_AV1E_SET_COLOR_PRIMARIES
			if (auto error = _factory->libaom_codec_control(&_ctx, AV1E_SET_COLOR_PRIMARIES, _settings.color_primaries);
				error != AOM_CODEC_OK) {
				const char* errstr = _factory->libaom_codec_err_to_string(error);
				const char* err    = _factory->libaom_codec_error(&_ctx);
				const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
				D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
							  "AV1E_SET_COLOR_PRIMARIES",                             //
							  (errstr ? errstr : ""), error,                          //
							  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
							  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
				);
			}
#else
			D_LOG_ERROR("AOM library was built without AV1E_SET_COLOR_PRIMARIES, behavior is unknown.");
#endif

#ifdef AOM_CTRL_AV1E_SET_TRANSFER_CHARACTERISTICS
			if (auto error =
					_factory->libaom_codec_control(&_ctx, AV1E_SET_TRANSFER_CHARACTERISTICS, _settings.color_trc);
				error != AOM_CODEC_OK) {
				const char* errstr = _factory->libaom_codec_err_to_string(error);
				const char* err    = _factory->libaom_codec_error(&_ctx);
				const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
				D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
							  "AV1E_SET_TRANSFER_CHARACTERISTICS",                    //
							  (errstr ? errstr : ""), error,                          //
							  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
							  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
				);
			}
#else
			D_LOG_ERROR("AOM library was built without AV1E_SET_TRANSFER_CHARACTERISTICS, behavior is unknown.");
#endif

#ifdef AOM_CTRL_AV1E_SET_MATRIX_COEFFICIENTS
			if (auto error =
					_factory->libaom_codec_control(&_ctx, AV1E_SET_MATRIX_COEFFICIENTS, _settings.color_matrix);
				error != AOM_CODEC_OK) {
				const char* errstr = _factory->libaom_codec_err_to_string(error);
				const char* err    = _factory->libaom_codec_error(&_ctx);
				const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
				D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
							  "AV1E_SET_MATRIX_COEFFICIENTS",                         //
							  (errstr ? errstr : ""), error,                          //
							  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
							  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
				);
			}
#else
			D_LOG_ERROR("AOM library was built without AV1E_SET_MATRIX_COEFFICIENTS, behavior is unknown.");
#endif

#ifdef AOM_CTRL_AV1E_SET_COLOR_RANGE
			if (auto error = _factory->libaom_codec_control(&_ctx, AV1E_SET_COLOR_RANGE, _settings.color_range);
				error != AOM_CODEC_OK) {
				const char* errstr = _factory->libaom_codec_err_to_string(error);
				const char* err    = _factory->libaom_codec_error(&_ctx);
				const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
				D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
							  "AV1E_SET_COLOR_RANGE",                                 //
							  (errstr ? errstr : ""), error,                          //
							  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
							  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
				);
			}
#else
			D_LOG_ERROR("AOM library was built without AV1_SET_COLOR_RANGE, behavior is unknown.");
#endif

#ifdef AOM_CTRL_AV1E_SET_CHROMA_SAMPLE_POSITION
			// !TODO: Consider making this user-controlled. At the moment, this follows the H.264 chroma standard.
			if (auto error = _factory->libaom_codec_control(&_ctx, AV1E_SET_CHROMA_SAMPLE_POSITION, AOM_CSP_VERTICAL);
				error != AOM_CODEC_OK) {
				const char* errstr = _factory->libaom_codec_err_to_string(error);
				const char* err    = _factory->libaom_codec_error(&_ctx);
				const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
				D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
							  "AV1E_SET_CHROMA_SAMPLE_POSITION",                      //
							  (errstr ? errstr : ""), error,                          //
							  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
							  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
				);
			}
#endif

#ifdef AOM_CTRL_AV1E_SET_RENDER_SIZE
			int32_t size[2] = {_settings.width, _settings.height};
			if (auto error = _factory->libaom_codec_control(&_ctx, AV1E_SET_RENDER_SIZE, &size);
				error != AOM_CODEC_OK) {
				const char* errstr = _factory->libaom_codec_err_to_string(error);
				const char* err    = _factory->libaom_codec_error(&_ctx);
				const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
				D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
							  "AV1E_SET_RENDER_SIZE",                                 //
							  (errstr ? errstr : ""), error,                          //
							  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
							  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
				);
			}
#endif
		}

		// Apply Dynamic Control Settings
		if (!update(settings)) {
			throw std::runtime_error("Unexpected error during configuration.");
		}
	}

	// Preallocate global headers.
	_global_headers = _factory->libaom_codec_get_global_headers(&_ctx);

	// Allocate frames.
	_images.resize(_cfg.g_threads);
	for (auto& image : _images) {
		_factory->libaom_img_alloc(&image, _settings.color_format, _settings.width, _settings.height, 8);

		// Color Information.
		image.fmt        = _settings.color_format;
		image.cp         = _settings.color_primaries;
		image.tc         = _settings.color_trc;
		image.mc         = _settings.color_matrix;
		image.range      = _settings.color_range;
		image.monochrome = _settings.monochrome ? 1 : 0;
		image.csp        = AOM_CSP_VERTICAL; // !TODO: Consider making this user-controlled.

		// Size
		image.r_w = image.d_w;
		image.r_h = image.d_h;
		image.r_w = image.w;
		image.r_h = image.h;
	}

	// Log Settings
	log();

	// Signal to future update() calls that we are fully initialized.
	_initialized = true;
}

aom_av1_instance::~aom_av1_instance()
{
#ifdef ENABLE_PROFILING
	// Profiling
	D_LOG_INFO("Timings | Avg. µs       | 99.9ile µs    | 99.0ile µs    | 95.0ile µs    | Samples  ", "");
	D_LOG_INFO("--------+---------------+---------------+---------------+---------------+----------", "");
	D_LOG_INFO("Copy    | %13.1f | %13" PRId64 " | %13" PRId64 " | %13" PRId64 " | %9" PRIu64,
			   _profiler_copy->average_duration() / 1000.,
			   std::chrono::duration_cast<std::chrono::microseconds>(_profiler_copy->percentile(0.999)).count(),
			   std::chrono::duration_cast<std::chrono::microseconds>(_profiler_copy->percentile(0.990)).count(),
			   std::chrono::duration_cast<std::chrono::microseconds>(_profiler_copy->percentile(0.950)).count(),
			   _profiler_copy->count());
	D_LOG_INFO("Encode  | %13.1f | %13" PRId64 " | %13" PRId64 " | %13" PRId64 " | %9" PRIu64,
			   _profiler_encode->average_duration() / 1000.,
			   std::chrono::duration_cast<std::chrono::microseconds>(_profiler_encode->percentile(0.999)).count(),
			   std::chrono::duration_cast<std::chrono::microseconds>(_profiler_encode->percentile(0.990)).count(),
			   std::chrono::duration_cast<std::chrono::microseconds>(_profiler_encode->percentile(0.950)).count(),
			   _profiler_encode->count());
	D_LOG_INFO("Packet  | %13.1f | %13" PRId64 " | %13" PRId64 " | %13" PRId64 " | %9" PRIu64,
			   _profiler_packet->average_duration() / 1000.,
			   std::chrono::duration_cast<std::chrono::microseconds>(_profiler_packet->percentile(0.999)).count(),
			   std::chrono::duration_cast<std::chrono::microseconds>(_profiler_packet->percentile(0.990)).count(),
			   std::chrono::duration_cast<std::chrono::microseconds>(_profiler_packet->percentile(0.950)).count(),
			   _profiler_packet->count());
#endif

	// Deallocate global buffer.
	if (_global_headers) {
		/* Breaks heap
		if (_global_headers->buf) {
			free(_global_headers->buf);
			_global_headers->buf = nullptr;
		}
		free(_global_headers);
		_global_headers = nullptr;
		*/
	}

	// Deallocate frames.
	for (auto& image : _images) {
		_factory->libaom_img_free(&image);
	}
	_images.clear();

	// Destroy encoder.
	_factory->libaom_codec_destroy(&_ctx);
}

void aom_av1_instance::migrate(obs_data_t* settings, uint64_t version) {}

bool aom_av1_instance::update(obs_data_t* settings)
{
	video_t*                        obsVideo      = obs_encoder_video(_self);
	const struct video_output_info* obsVideoInfo  = video_output_get_info(obsVideo);
	uint32_t                        obsFPSnum     = obsVideoInfo->fps_num;
	uint32_t                        obsFPSden     = obsVideoInfo->fps_den;
	bool                            obsMonochrome = (obsVideoInfo->format == VIDEO_FORMAT_Y800);

#define SET_IF_NOT_DEFAULT(X, Y)         \
	if (X != -1) {                       \
		Y = static_cast<decltype(Y)>(X); \
	}

	{ // Generate Dynamic Settings

		{ // Encoder
			_settings.preset = static_cast<int8_t>(obs_data_get_int(settings, ST_KEY_ENCODER_CPUUSAGE));
		}

		{ // Rate Control
			_settings.rc_bitrate = static_cast<int32_t>(obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE));
			_settings.rc_bitrate_overshoot =
				static_cast<int32_t>(obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_UNDERSHOOT));
			_settings.rc_bitrate_undershoot =
				static_cast<int32_t>(obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_OVERSHOOT));
			_settings.rc_quality = static_cast<int32_t>(obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_QUALITY));
			_settings.rc_quantizer_min =
				static_cast<int32_t>(obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_QUANTIZER_MINIMUM));
			_settings.rc_quantizer_max =
				static_cast<int32_t>(obs_data_get_int(settings, ST_KEY_RATECONTROL_LIMITS_QUANTIZER_MAXIMUM));
			_settings.rc_buffer_ms = static_cast<int32_t>(obs_data_get_int(settings, ST_KEY_RATECONTROL_BUFFER_SIZE));
			_settings.rc_buffer_initial_ms =
				static_cast<int32_t>(obs_data_get_int(settings, ST_KEY_RATECONTROL_BUFFER_SIZE_INITIAL));
			_settings.rc_buffer_optimal_ms =
				static_cast<int32_t>(obs_data_get_int(settings, ST_KEY_RATECONTROL_BUFFER_SIZE_OPTIMAL));
		}

		{ // Key-Frames
			int64_t kf_type    = obs_data_get_int(settings, ST_KEY_KEYFRAMES_INTERVALTYPE);
			bool    is_seconds = (kf_type == 0);

			_settings.kf_mode = AOM_KF_AUTO;
			if (is_seconds) {
				_settings.kf_distance_max = static_cast<unsigned int>(
					std::lround(obs_data_get_double(settings, ST_KEY_KEYFRAMES_INTERVAL_SECONDS)
								* static_cast<double>(obsFPSnum) / static_cast<double>(obsFPSden)));
			} else {
				_settings.kf_distance_max =
					static_cast<unsigned int>(obs_data_get_int(settings, ST_KEY_KEYFRAMES_INTERVAL_FRAMES));
			}
			_settings.kf_distance_min = _settings.kf_distance_max;
		}

		// All-Intra requires us to never set Key-Frames.
		if (_cfg.g_usage == AOM_USAGE_ALL_INTRA) {
			_settings.rc_lookahead    = 0;
			_settings.kf_mode         = AOM_KF_DISABLED;
			_settings.kf_distance_min = 0;
			_settings.kf_distance_max = 0;
		}
	}

	{ // Configuration.

		{ // Usage and Defaults
			_cfg.g_usage = static_cast<unsigned int>(obs_data_get_int(settings, ST_KEY_ENCODER_USAGE));
			_factory->libaom_codec_enc_config_default(_iface, &_cfg, _cfg.g_usage);
		}

		{ // Frame Information
			// Size
			_cfg.g_w = _settings.width;
			_cfg.g_h = _settings.height;

			// Time Base (Rate is inverted Time Base)
			_cfg.g_timebase.num = _settings.fps.den;
			_cfg.g_timebase.den = _settings.fps.num;

			// !INFO: Whenever OBS decides to support anything but 8-bits, let me know.
			_cfg.g_bit_depth       = AOM_BITS_8;
			_cfg.g_input_bit_depth = AOM_BITS_8;

			// Monochrome color
			_cfg.monochrome = _settings.monochrome ? 1 : 0;
		}

		{ // Encoder

			// AV1 Profile
			_cfg.g_profile = static_cast<unsigned int>(_settings.profile);
		}

		{ // Rate Control
			// Mode
			_cfg.rc_end_usage = static_cast<aom_rc_mode>(obs_data_get_int(settings, ST_KEY_RATECONTROL_MODE));

			// Look-Ahead
			SET_IF_NOT_DEFAULT(_settings.rc_lookahead, _cfg.g_lag_in_frames);

			// Limits
			SET_IF_NOT_DEFAULT(_settings.rc_bitrate, _cfg.rc_target_bitrate);
			SET_IF_NOT_DEFAULT(_settings.rc_bitrate_overshoot, _cfg.rc_overshoot_pct);
			SET_IF_NOT_DEFAULT(_settings.rc_bitrate_undershoot, _cfg.rc_undershoot_pct);
			SET_IF_NOT_DEFAULT(_settings.rc_quantizer_min, _cfg.rc_min_quantizer);
			SET_IF_NOT_DEFAULT(_settings.rc_quantizer_max, _cfg.rc_max_quantizer);

			// Buffer
			SET_IF_NOT_DEFAULT(_settings.rc_buffer_ms, _cfg.rc_buf_sz);
			SET_IF_NOT_DEFAULT(_settings.rc_buffer_initial_ms, _cfg.rc_buf_initial_sz);
			SET_IF_NOT_DEFAULT(_settings.rc_buffer_optimal_ms, _cfg.rc_buf_optimal_sz);
		}

		{ // Key-Frames
			SET_IF_NOT_DEFAULT(_settings.kf_mode, _cfg.kf_mode);
			SET_IF_NOT_DEFAULT(_settings.kf_distance_min, _cfg.kf_min_dist);
			SET_IF_NOT_DEFAULT(_settings.kf_distance_max, _cfg.kf_max_dist);
		}

		{ // Advanced

			// Single-Pass
			_cfg.g_pass = AOM_RC_ONE_PASS;

			// Threading
			SET_IF_NOT_DEFAULT(_settings.threads, _cfg.g_threads);
		}

		// TODO: Future
		//_cfg.rc_resize_mode = 0; // "RESIZE_NONE
		//_cfg.rc_resize_denominator = ?;
		//_cfg.rc_resize_kf_denominator = ?;
		//_cfg.rc_superres_mode = AOM_SUPERRES_NONE;
		//_cfg.rc_superres_denominator = ?;
		//_cfg.rc_superres_kf_denominator = ?;
		//_cfg.rc_superres_qthresh = ?;
		//_cfg.rc_superres_kf_qtresh = ?;
		//_cfg.rc_dropframe_thresh = ?;
		//_cfg.fwd_kf_enabled = ?;
		//_cfg.g_forced_max_frame_width = ?;
		//_cfg.g_forced_max_frame_height = ?;
		//_cfg.g_error_resilient = ?;
		//_cfg.g_lag_in_frames = ?;
		//_cfg.sframe_dist = ?;
		//_cfg.sframe_mode      = 0;
		//_cfg.large_scale_tile = 0;
		//_cfg.full_still_picture_hdr = ?;
		//_cfg.save_as_annexb = ?;
		//_cfg.encoder_cfg = ?;

		// Apply configuration
		if (_initialized) {
			if (auto error = _factory->libaom_codec_enc_config_set(&_ctx, &_cfg); error != AOM_CODEC_OK) {
				const char* errstr = _factory->libaom_codec_err_to_string(error);
				const char* err    = _factory->libaom_codec_error(&_ctx);
				const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
				D_LOG_WARNING("Error changing configuration: %s (code %" PRIu32 ")%s%s%s%s", //
							  (errstr ? errstr : ""), error,                                 //
							  (err ? "\n\tMessage: " : ""), (err ? err : ""),                //
							  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "")        //
				);
				return false;
			}
		}
	}

	if (_ctx.iface) { // Control

		{ // Encoder
#ifdef AOM_CTRL_AOME_SET_CPUUSED
			if (_settings.preset != -1) {
				if (auto error = _factory->libaom_codec_control(&_ctx, AOME_SET_CPUUSED, _settings.preset);
					error != AOM_CODEC_OK) {
					const char* errstr = _factory->libaom_codec_err_to_string(error);
					const char* err    = _factory->libaom_codec_error(&_ctx);
					const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
					D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
								  "AOME_SET_CPUUSED",                                     //
								  (errstr ? errstr : ""), error,                          //
								  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
								  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
					);
				}
			}
#endif
		}

		{ // Rate Control
#ifdef AOM_CTRL_AOME_SET_CQ_LEVEL
			if ((_settings.rc_quality != -1) && ((_settings.rc_mode == AOM_CQ) || (_settings.rc_mode == AOM_Q))) {
				if (auto error = _factory->libaom_codec_control(&_ctx, AOME_SET_CQ_LEVEL, _settings.rc_quality);
					error != AOM_CODEC_OK) {
					const char* errstr = _factory->libaom_codec_err_to_string(error);
					const char* err    = _factory->libaom_codec_error(&_ctx);
					const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
					D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
								  "AOME_SET_CQ_LEVEL",                                    //
								  (errstr ? errstr : ""), error,                          //
								  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
								  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
					);
				}
			}
#endif
		}

		{ // Advanced
#ifdef AOM_CTRL_AV1E_SET_ROW_MT
			if (_settings.rowmultithreading != -1) {
				if (auto error = _factory->libaom_codec_control(&_ctx, AV1E_SET_ROW_MT, _settings.rowmultithreading);
					error != AOM_CODEC_OK) {
					const char* errstr = _factory->libaom_codec_err_to_string(error);
					const char* err    = _factory->libaom_codec_error(&_ctx);
					const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
					D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
								  "AV1E_SET_ROW_MT",                                      //
								  (errstr ? errstr : ""), error,                          //
								  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
								  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
					);
				}
			}
#endif

#ifdef AOM_CTRL_AV1E_SET_TILE_COLUMNS
			if (_settings.tile_columns != -1) {
				if (auto error = _factory->libaom_codec_control(&_ctx, AV1E_SET_TILE_COLUMNS, _settings.tile_columns);
					error != AOM_CODEC_OK) {
					const char* errstr = _factory->libaom_codec_err_to_string(error);
					const char* err    = _factory->libaom_codec_error(&_ctx);
					const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
					D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
								  "AV1E_SET_TILE_COLUMNS",                                //
								  (errstr ? errstr : ""), error,                          //
								  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
								  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
					);
				}
			}
#endif

#ifdef AOM_CTRL_AV1E_SET_TILE_ROWS
			if (_settings.tile_rows != -1) {
				if (auto error = _factory->libaom_codec_control(&_ctx, AV1E_SET_TILE_ROWS, _settings.tile_rows);
					error != AOM_CODEC_OK) {
					const char* errstr = _factory->libaom_codec_err_to_string(error);
					const char* err    = _factory->libaom_codec_error(&_ctx);
					const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
					D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
								  "AV1E_SET_TILE_ROWS",                                   //
								  (errstr ? errstr : ""), error,                          //
								  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
								  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
					);
				}
			}
#endif
#ifdef AOM_CTRL_AOME_SET_TUNING
			if (_settings.tune_metric != -1) {
				if (auto error = _factory->libaom_codec_control(&_ctx, AOME_SET_TUNING, _settings.tune_metric);
					error != AOM_CODEC_OK) {
					const char* errstr = _factory->libaom_codec_err_to_string(error);
					const char* err    = _factory->libaom_codec_error(&_ctx);
					const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
					D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
								  "AOME_SET_TUNING",                                      //
								  (errstr ? errstr : ""), error,                          //
								  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
								  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
					);
				}
			}
#endif
#ifdef AOM_CTRL_AV1E_SET_TUNE_CONTENT
			if (_settings.tune_content != AOM_CONTENT_DEFAULT) {
				if (auto error = _factory->libaom_codec_control(&_ctx, AV1E_SET_TUNE_CONTENT, _settings.tune_content);
					error != AOM_CODEC_OK) {
					const char* errstr = _factory->libaom_codec_err_to_string(error);
					const char* err    = _factory->libaom_codec_error(&_ctx);
					const char* errdtl = _factory->libaom_codec_error_detail(&_ctx);
					D_LOG_WARNING("Error changing '%s': %s (code %" PRIu32 ")%s%s%s%s",   //
								  "AV1E_SET_TUNE_CONTENT",                                //
								  (errstr ? errstr : ""), error,                          //
								  (err ? "\n\tMessage: " : ""), (err ? err : ""),         //
								  (errdtl ? "\n\tDetails: " : ""), (errdtl ? errdtl : "") //
					);
				}
			}
#endif
		}
	}

#undef SET_IF_NOT_DEFAULT

	// Log the changed settings.
	if (_initialized) {
		log();
	}

	return true;
}

void aom_av1_instance::log()
{
	D_LOG_INFO("AOM AV1:", "");
	D_LOG_INFO("  Video: %" PRIu16 "x%" PRIu16 "@%1.2ffps (%" PRIu32 "/%" PRIu32 ")", _settings.width, _settings.height,
			   static_cast<double>(_settings.fps.num) / static_cast<float>(_settings.fps.den), _settings.fps.num,
			   _settings.fps.den);
	D_LOG_INFO("  Color: %s/%s/%s%s", aom_color_format_to_string(_settings.color_format),
			   aom_color_trc_to_string(_settings.color_trc),
			   _settings.color_range == AOM_CR_FULL_RANGE ? "Full" : "Partial",
			   _settings.monochrome ? "/Monochrome" : "");

	// Rate Control
	D_LOG_INFO("  Rate Control: %s", aom_rc_mode_to_string(_settings.rc_mode));
	D_LOG_INFO("    Look-Ahead: %" PRId8, _settings.rc_lookahead);
	D_LOG_INFO("    Buffers: %" PRId32 " ms / %" PRId32 " ms / %" PRId32 " ms", _settings.rc_buffer_ms,
			   _settings.rc_buffer_initial_ms, _settings.rc_buffer_optimal_ms);
	D_LOG_INFO("    Bitrate: %" PRId32 " kbit/s (-%" PRId32 "%% - +%" PRId32 "%%)", _settings.rc_bitrate,
			   _settings.rc_bitrate_undershoot, _settings.rc_bitrate_overshoot);
	D_LOG_INFO("    Quality: %" PRId8, _settings.rc_quality);
	D_LOG_INFO("    Quantizer: %" PRId8 " - %" PRId8, _settings.rc_quantizer_min, _settings.rc_quantizer_max);

	// Key-Frames
	D_LOG_INFO("  Key-Frames: %s", aom_kf_mode_to_string(_settings.kf_mode));
	D_LOG_INFO("    Distance: %" PRId32 " - %" PRId32 " frames", _settings.kf_distance_min, _settings.kf_distance_max);

	// Advanced
	D_LOG_INFO("  Advanced: ", "");
	D_LOG_INFO("   Threads: %" PRId8, _settings.threads);
	D_LOG_INFO("   Row-Multi-Threading: %s", _settings.rowmultithreading == -1  ? "Default"
											 : _settings.rowmultithreading == 1 ? "Enabled"
																				: "Disabled");
	D_LOG_INFO("   Tiling: %" PRId8 "x%" PRId8, _settings.tile_columns, _settings.tile_rows);
	D_LOG_INFO("   Tune: %s (Metric), %s (Content)", aom_tune_metric_to_string(_settings.tune_metric),
			   aom_tune_content_to_string(_settings.tune_content));
}

bool aom_av1_instance::get_extra_data(uint8_t** extra_data, size_t* size)
{
	if (!_global_headers) {
		return false;
	}

	*extra_data = static_cast<uint8_t*>(_global_headers->buf);
	*size       = _global_headers->sz;

	return true;
}

bool aom_av1_instance::get_sei_data(uint8_t** sei_data, size_t* size)
{
	return get_extra_data(sei_data, size);
}

void aom_av1_instance::get_video_info(struct video_scale_info* info)
{
	// Fix up color format.
	auto format = obs_encoder_get_preferred_video_format(_self);
	if (format == VIDEO_FORMAT_NONE) {
		format = info->format;
	}

	switch (format) {
		// Perfect matches.
	case VIDEO_FORMAT_I444: // AOM_IMG_I444.
	case VIDEO_FORMAT_I422: // AOM_IMG_I422.
	case VIDEO_FORMAT_I420: // AOM_IMG_I420.
		break;

		// 4:2:0 formats
	case VIDEO_FORMAT_NV12: // Y, UV Interleaved.
	case VIDEO_FORMAT_I40A:
		D_LOG_WARNING("Color-format '%s' is not supported, forcing 'I420'...", obs_video_format_to_string(format));
		info->format = VIDEO_FORMAT_I420;
		break;

		// 4:2:2-like formats
	case VIDEO_FORMAT_UYVY:
	case VIDEO_FORMAT_YUY2:
	case VIDEO_FORMAT_YVYU:
	case VIDEO_FORMAT_I42A:
		D_LOG_WARNING("Color-format '%s' is not supported, forcing 'I422'...", obs_video_format_to_string(format));
		info->format = VIDEO_FORMAT_I422;
		break;

		// 4:4:4
	case VIDEO_FORMAT_BGR3:
	case VIDEO_FORMAT_BGRA:
	case VIDEO_FORMAT_BGRX:
	case VIDEO_FORMAT_RGBA:
	case VIDEO_FORMAT_YUVA:
	case VIDEO_FORMAT_AYUV:
	case VIDEO_FORMAT_Y800: // Grayscale, no exact match.
		D_LOG_WARNING("Color-format '%s' is not supported, forcing 'I444'...", obs_video_format_to_string(format));
		info->format = VIDEO_FORMAT_I444;
		break;
	}

	// Fix up color space.
	if (info->colorspace == VIDEO_CS_DEFAULT) {
		info->colorspace = VIDEO_CS_SRGB;
	}

	// Fix up color range.
	if (info->range == VIDEO_RANGE_DEFAULT) {
		info->range = VIDEO_RANGE_PARTIAL;
	}
}

bool streamfx::encoder::aom::av1::aom_av1_instance::encode_video(encoder_frame* frame, encoder_packet* packet,
																 bool* received_packet)
{
	// Retrieve current indexed image.
	auto& image = _images.at(_image_index);

	{ // Copy Image data.
#ifdef ENABLE_PROFILING
		auto profile = _profiler_copy->track();
#endif
		std::memcpy(image.planes[AOM_PLANE_Y], frame->data[0], frame->linesize[0] * image.h);
		if (image.fmt == AOM_IMG_FMT_I420) {
			std::memcpy(image.planes[AOM_PLANE_U], frame->data[1], frame->linesize[1] * image.h / 2);
			std::memcpy(image.planes[AOM_PLANE_V], frame->data[2], frame->linesize[2] * image.h / 2);
		} else {
			std::memcpy(image.planes[AOM_PLANE_U], frame->data[1], frame->linesize[1] * image.h);
			std::memcpy(image.planes[AOM_PLANE_V], frame->data[2], frame->linesize[2] * image.h);
		}
	}

	{ // Try to encode the new image.
#ifdef ENABLE_PROFILING
		auto profile = _profiler_encode->track();
#endif
		aom_enc_frame_flags_t flags = 0;
		if (_cfg.g_usage == AOM_USAGE_ALL_INTRA) {
			flags = AOM_EFLAG_FORCE_KF;
		}
		if (auto error = _factory->libaom_codec_encode(&_ctx, &image, frame->pts, 1, flags); error != AOM_CODEC_OK) {
			const char* errstr = _factory->libaom_codec_err_to_string(error);
			D_LOG_ERROR("Encoding frame failed with error: %s (code %" PRIu32 ")\n%s\n%s", errstr, error,
						_factory->libaom_codec_error(&_ctx), _factory->libaom_codec_error_detail(&_ctx));
			return false;
		} else {
			// Increment the image index.
			_image_index = (_image_index++) % _images.size();
		}
	}

	{ // Get Packet
#ifdef ENABLE_PROFILING
		auto profile = _profiler_packet->track();
#endif
		aom_codec_iter_t iter = NULL;
		for (auto* pkt = _factory->libaom_codec_get_cx_data(&_ctx, &iter); pkt != nullptr;
			 pkt       = _factory->libaom_codec_get_cx_data(&_ctx, &iter)) {
#ifdef _DEBUG
			{
				const char* kind = "";
				switch (pkt->kind) {
				case AOM_CODEC_CX_FRAME_PKT:
					kind = "Frame";
					break;
				case AOM_CODEC_STATS_PKT:
					kind = "Stats";
					break;
				case AOM_CODEC_FPMB_STATS_PKT:
					kind = "FPMB Stats";
					break;
				case AOM_CODEC_PSNR_PKT:
					kind = "PSNR";
					break;
				case AOM_CODEC_CUSTOM_PKT:
					kind = "Custom";
					break;
				}
				D_LOG_DEBUG("\tPacket: Kind=%s", kind)
			}
#endif

			if (pkt->kind == AOM_CODEC_CX_FRAME_PKT) {
				// Status
				packet->type     = OBS_ENCODER_VIDEO;
				packet->keyframe = ((pkt->data.frame.flags & AOM_FRAME_IS_KEY) == AOM_FRAME_IS_KEY)
								   || (_cfg.g_usage == AOM_USAGE_ALL_INTRA);
				if (packet->keyframe) {
					//
					packet->priority      = 3; // OBS_NAL_PRIORITY_HIGHEST
					packet->drop_priority = 3; // OBS_NAL_PRIORITY_HIGHEST
				} else if ((pkt->data.frame.flags & AOM_FRAME_IS_DROPPABLE) != AOM_FRAME_IS_DROPPABLE) {
					// Dropping this frame breaks the bitstream.
					packet->priority      = 2; // OBS_NAL_PRIORITY_HIGH
					packet->drop_priority = 3; // OBS_NAL_PRIORITY_HIGHEST
				} else {
					// This frame can be dropped at will.
					packet->priority      = 0; // OBS_NAL_PRIORITY_DISPOSABLE
					packet->drop_priority = 0; // OBS_NAL_PRIORITY_DISPOSABLE
				}

				// Data
				packet->data = static_cast<uint8_t*>(pkt->data.frame.buf);
				packet->size = pkt->data.frame.sz;

				// Timestamps
				//TODO: Temporarily set both to the same until there is a way to figure out actual order.
				packet->pts = pkt->data.frame.pts;
				packet->dts = pkt->data.frame.pts;

				*received_packet = true;
			}

			if (*received_packet == true)
				break;
		}

		if (!*received_packet) {
			packet->type = OBS_ENCODER_VIDEO;
			packet->data = nullptr;
			packet->size = 0;
			packet->pts  = -1;
			packet->dts  = -1;
#ifdef _DEBUG
			D_LOG_DEBUG("No Packet", "");
#endif
			// Not necessarily an error.
			//return false;
		} else {
#ifdef _DEBUG
			D_LOG_DEBUG("Packet: Type=%s PTS=%06" PRId64 " DTS=%06" PRId64 " Size=%016" PRIuPTR "",
						packet->keyframe ? "I" : "P", packet->pts, packet->dts, packet->size);
#endif
		}
	}

	return true;
}

aom_av1_factory::aom_av1_factory()
{
	// Try and load the AOM library.
	std::vector<std::filesystem::path> libs;

#ifdef D_PLATFORM_WINDOWS
	// Try loading from the data directory first.
	libs.push_back(streamfx::data_file_path("aom.dll"));    // MSVC (preferred)
	libs.push_back(streamfx::data_file_path("libaom.dll")); // Cross-Compile
	// In any other case, load the system-wide binary.
	libs.push_back("aom.dll");
	libs.push_back("libaom.dll");
#else
	// Try loading from the data directory first.
	libs.push_back(streamfx::data_file_path("libaom.so"));
	// In any other case, load the system-wide binary.
	libs.push_back("libaom");
#endif

	for (auto lib : libs) {
		try {
			_library = streamfx::util::library::load(lib);
			if (_library)
				break;
		} catch (...) {
			D_LOG_WARNING("Loading of '%s' failed.", lib.generic_string().c_str());
		}
	}
	if (!_library) {
		throw std::runtime_error("Unable to load AOM library.");
	}

	// Load all necessary functions.
#define _LOAD_SYMBOL(X)                                                                      \
	{                                                                                        \
		lib##X = reinterpret_cast<decltype(lib##X)>(_library->load_symbol(std::string(#X))); \
	}
#define _LOAD_SYMBOL_(X, Y)                                                                 \
	{                                                                                       \
		lib##X = reinterpret_cast<decltype(lib##X)>(_library->load_symbol(std::string(Y))); \
	}
	_LOAD_SYMBOL(aom_codec_version);
	_LOAD_SYMBOL(aom_codec_version_str);
	_LOAD_SYMBOL(aom_codec_version_extra_str);
	_LOAD_SYMBOL(aom_codec_build_config);
	_LOAD_SYMBOL(aom_codec_iface_name);
	_LOAD_SYMBOL(aom_codec_err_to_string);
	_LOAD_SYMBOL(aom_codec_error);
	_LOAD_SYMBOL(aom_codec_error_detail);
	_LOAD_SYMBOL(aom_codec_destroy);
	_LOAD_SYMBOL(aom_codec_get_caps);
	_LOAD_SYMBOL(aom_codec_control);
	_LOAD_SYMBOL(aom_codec_set_option);
	_LOAD_SYMBOL(aom_obu_type_to_string);
	_LOAD_SYMBOL(aom_uleb_size_in_bytes);
	_LOAD_SYMBOL(aom_uleb_decode);
	_LOAD_SYMBOL(aom_uleb_encode);
	_LOAD_SYMBOL(aom_uleb_encode_fixed_size);
	_LOAD_SYMBOL(aom_img_alloc);
	_LOAD_SYMBOL(aom_img_wrap);
	_LOAD_SYMBOL(aom_img_alloc_with_border);
	_LOAD_SYMBOL(aom_img_set_rect);
	_LOAD_SYMBOL(aom_img_flip);
	_LOAD_SYMBOL(aom_img_free);
	_LOAD_SYMBOL(aom_img_plane_width);
	_LOAD_SYMBOL(aom_img_plane_height);
	_LOAD_SYMBOL(aom_img_add_metadata);
	_LOAD_SYMBOL(aom_img_get_metadata);
	_LOAD_SYMBOL(aom_img_num_metadata);
	_LOAD_SYMBOL(aom_img_remove_metadata);
	_LOAD_SYMBOL(aom_img_metadata_alloc);
	_LOAD_SYMBOL(aom_img_metadata_free);
	_LOAD_SYMBOL(aom_codec_enc_init_ver);
	_LOAD_SYMBOL(aom_codec_enc_config_default);
	_LOAD_SYMBOL(aom_codec_enc_config_set);
	_LOAD_SYMBOL(aom_codec_get_global_headers);
	_LOAD_SYMBOL(aom_codec_encode);
	_LOAD_SYMBOL(aom_codec_set_cx_data_buf);
	_LOAD_SYMBOL(aom_codec_get_cx_data);
	_LOAD_SYMBOL(aom_codec_get_preview_frame);
	_LOAD_SYMBOL(aom_codec_av1_cx);
#undef _LOAD_SYMBOL

	// Register encoder.
	_info.id    = S_PREFIX "aom-av1";
	_info.type  = obs_encoder_type::OBS_ENCODER_VIDEO;
	_info.codec = "av1";
	_info.caps  = OBS_ENCODER_CAP_DYN_BITRATE;
	_info.caps |= OBS_ENCODER_CAP_DEPRECATED;

	finish_setup();
}

aom_av1_factory::~aom_av1_factory() {}

std::shared_ptr<aom_av1_factory> _aom_av1_factory_instance = nullptr;

void aom_av1_factory::initialize()
try {
	if (!_aom_av1_factory_instance) {
		_aom_av1_factory_instance = std::make_shared<aom_av1_factory>();
	}
} catch (std::exception const& ex) {
	D_LOG_ERROR("Failed to initialize AOM AV1 encoder: %s", ex.what());
}

void aom_av1_factory::finalize()
{
	_aom_av1_factory_instance.reset();
}

std::shared_ptr<aom_av1_factory> aom_av1_factory::get()
{
	return _aom_av1_factory_instance;
}

const char* aom_av1_factory::get_name()
{
	return "AV1 (via AOM)";
}

void* aom_av1_factory::create(obs_data_t* settings, obs_encoder_t* encoder, bool is_hw)
{
	return new aom_av1_instance(settings, encoder, is_hw);
}

void aom_av1_factory::get_defaults2(obs_data_t* settings)
{
	{ // Presets
		obs_data_set_default_int(settings, ST_KEY_ENCODER_USAGE, static_cast<long long>(AOM_USAGE_REALTIME));
		obs_data_set_default_int(settings, ST_KEY_ENCODER_CPUUSAGE, -1);
		obs_data_set_default_int(settings, ST_KEY_ENCODER_PROFILE,
								 static_cast<long long>(codec::av1::profile::UNKNOWN));
	}

	{ // Rate-Control
		obs_data_set_default_int(settings, ST_KEY_RATECONTROL_MODE, static_cast<long long>(AOM_CBR));

		// Limits
		obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE, 6000);
		obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_UNDERSHOOT, -1);
		obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_BITRATE_OVERSHOOT, -1);
		obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_QUALITY, -1);
		obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_QUANTIZER_MINIMUM, -1);
		obs_data_set_default_int(settings, ST_KEY_RATECONTROL_LIMITS_QUANTIZER_MAXIMUM, -1);

		// Buffer
		obs_data_set_default_int(settings, ST_KEY_RATECONTROL_BUFFER_SIZE, -1);
		obs_data_set_default_int(settings, ST_KEY_RATECONTROL_BUFFER_SIZE_INITIAL, -1);
		obs_data_set_default_int(settings, ST_KEY_RATECONTROL_BUFFER_SIZE_OPTIMAL, -1);
	}

	{ // Key-Frame Options
		obs_data_set_default_int(settings, ST_KEY_KEYFRAMES_INTERVALTYPE, 0);
		obs_data_set_default_double(settings, ST_KEY_KEYFRAMES_INTERVAL_SECONDS, 2.0);
		obs_data_set_default_int(settings, ST_KEY_KEYFRAMES_INTERVAL_FRAMES, 300);
	}

	{ // Advanced Options
		obs_data_set_default_int(settings, ST_KEY_ADVANCED_THREADS, 0);
		obs_data_set_default_int(settings, ST_KEY_ADVANCED_ROWMULTITHREADING, -1);
		obs_data_set_default_int(settings, ST_KEY_ADVANCED_TILE_COLUMNS, -1);
		obs_data_set_default_int(settings, ST_KEY_ADVANCED_TILE_ROWS, -1);
		obs_data_set_default_int(settings, ST_KEY_ADVANCED_TUNE_METRIC, -1);
		obs_data_set_default_int(settings, ST_KEY_ADVANCED_TUNE_CONTENT, static_cast<long long>(AOM_CONTENT_DEFAULT));
	}
}

static bool modified_usage(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept
try {
	bool is_all_intra = false;
	if (obs_data_get_int(settings, ST_KEY_ENCODER_USAGE) == AOM_USAGE_ALL_INTRA) {
		is_all_intra = true;
	}

	// All-Intra does not support these.
	obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LOOKAHEAD), !is_all_intra);
	obs_property_set_visible(obs_properties_get(props, ST_I18N_KEYFRAMES), !is_all_intra);

	return true;
} catch (const std::exception& ex) {
	DLOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what());
	return false;
} catch (...) {
	DLOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__);
	return false;
}

static bool modified_ratecontrol_mode(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept
try {
	bool is_bitrate_visible        = false;
	bool is_overundershoot_visible = false;
	bool is_quality_visible        = false;

	// Fix rate control mode selection if ALL_INTRA is selected.
	if (obs_data_get_int(settings, ST_KEY_ENCODER_USAGE) == AOM_USAGE_ALL_INTRA) {
		obs_data_set_int(settings, ST_KEY_RATECONTROL_MODE, static_cast<long long>(aom_rc_mode::AOM_Q));
	}

	{ // Based on the Rate Control Mode, show and hide options.
		auto mode = static_cast<aom_rc_mode>(obs_data_get_int(settings, ST_KEY_RATECONTROL_MODE));
		if (mode == AOM_CBR) {
			is_bitrate_visible        = true;
			is_overundershoot_visible = true;
		} else if (mode == AOM_VBR) {
			is_bitrate_visible        = true;
			is_overundershoot_visible = true;
		} else if (mode == AOM_CQ) {
			is_bitrate_visible        = true;
			is_overundershoot_visible = true;
			is_quality_visible        = true;
		} else if (mode == AOM_Q) {
			is_quality_visible = true;
		}

		obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BITRATE), is_bitrate_visible);
		obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BITRATE_UNDERSHOOT),
								 is_overundershoot_visible);
		obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_BITRATE_OVERSHOOT),
								 is_overundershoot_visible);
#ifdef AOM_CTRL_AOME_SET_CQ_LEVEL
		obs_property_set_visible(obs_properties_get(props, ST_KEY_RATECONTROL_LIMITS_QUALITY), is_quality_visible);
#endif
	}
	return true;
} catch (const std::exception& ex) {
	DLOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what());
	return false;
} catch (...) {
	DLOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__);
	return false;
}

static bool modified_keyframes(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept
try {
	bool is_seconds = obs_data_get_int(settings, ST_KEY_KEYFRAMES_INTERVALTYPE) == 0;
	obs_property_set_visible(obs_properties_get(props, ST_KEY_KEYFRAMES_INTERVAL_FRAMES), !is_seconds);
	obs_property_set_visible(obs_properties_get(props, ST_KEY_KEYFRAMES_INTERVAL_SECONDS), is_seconds);
	return true;
} catch (const std::exception& ex) {
	DLOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what());
	return false;
} catch (...) {
	DLOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__);
	return false;
}

obs_properties_t* aom_av1_factory::get_properties2(instance_t* data)
{
	obs_properties_t* props = obs_properties_create();

	{
		auto p = obs_properties_add_text(props, "[[deprecated]]", D_TRANSLATE(ST_I18N_DEPRECATED), OBS_TEXT_INFO);
		obs_property_text_set_info_type(p, OBS_TEXT_INFO_WARNING);
		obs_property_text_set_info_word_wrap(p, true);
	}

#ifdef ENABLE_FRONTEND
	{
		obs_properties_add_button2(props, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), aom_av1_factory::on_manual_open,
								   this);
	}
#endif

	{ // Presets
		obs_properties_t* grp = obs_properties_create();
		obs_properties_add_group(props, ST_I18N_ENCODER, D_TRANSLATE(ST_I18N_ENCODER), OBS_GROUP_NORMAL, grp);
		//obs_properties_add_group(props, S_CODEC_AV1, D_TRANSLATE(S_CODEC_AV1), OBS_GROUP_NORMAL, grp);

		{ // Usage
			auto p = obs_properties_add_list(grp, ST_KEY_ENCODER_USAGE, D_TRANSLATE(ST_I18N_ENCODER_USAGE),
											 OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
			obs_property_set_modified_callback(p, modified_usage);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_USAGE_GOODQUALITY),
									  static_cast<long long>(AOM_USAGE_GOOD_QUALITY));
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_USAGE_REALTIME),
									  static_cast<long long>(AOM_USAGE_REALTIME));
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_USAGE_ALLINTRA),
									  static_cast<long long>(AOM_USAGE_ALL_INTRA));
		}

#ifdef AOM_CTRL_AOME_SET_CPUUSED
		{ // CPU Usage
			auto p = obs_properties_add_list(grp, ST_KEY_ENCODER_CPUUSAGE, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE),
											 OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
			obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DEFAULT), -1);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE_10), 10);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE_9), 9);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE_8), 8);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE_7), 7);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE_6), 6);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE_5), 5);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE_4), 4);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE_3), 3);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE_2), 2);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE_1), 1);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ENCODER_CPUUSAGE_0), 0);
		}
#endif

		{ // Profile
			auto p = obs_properties_add_list(grp, ST_KEY_ENCODER_PROFILE, D_TRANSLATE(S_CODEC_AV1_PROFILE),
											 OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
			obs_property_list_add_int(p, D_TRANSLATE(S_STATE_AUTOMATIC),
									  static_cast<long long>(codec::av1::profile::UNKNOWN));
			obs_property_list_add_int(p, codec::av1::profile_to_string(codec::av1::profile::MAIN),
									  static_cast<long long>(codec::av1::profile::MAIN));
			obs_property_list_add_int(p, codec::av1::profile_to_string(codec::av1::profile::HIGH),
									  static_cast<long long>(codec::av1::profile::HIGH));
			obs_property_list_add_int(p, codec::av1::profile_to_string(codec::av1::profile::PROFESSIONAL),
									  static_cast<long long>(codec::av1::profile::PROFESSIONAL));
		}
	}

	{ // Rate Control Options
		obs_properties_t* grp = obs_properties_create();
		obs_properties_add_group(props, ST_I18N_RATECONTROL, D_TRANSLATE(ST_I18N_RATECONTROL), OBS_GROUP_NORMAL, grp);

		{ // Mode
			auto p = obs_properties_add_list(grp, ST_KEY_RATECONTROL_MODE, D_TRANSLATE(ST_I18N_RATECONTROL_MODE),
											 OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
			obs_property_set_modified_callback(p, modified_ratecontrol_mode);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_RATECONTROL_MODE_VBR), static_cast<long long>(AOM_VBR));
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_RATECONTROL_MODE_CBR), static_cast<long long>(AOM_CBR));
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_RATECONTROL_MODE_CQ), static_cast<long long>(AOM_CQ));
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_RATECONTROL_MODE_Q), static_cast<long long>(AOM_Q));
		}

		{ // Look-Ahead
			auto p =
				obs_properties_add_int(grp, ST_KEY_RATECONTROL_LOOKAHEAD, D_TRANSLATE(ST_I18N_RATECONTROL_LOOKAHEAD),
									   -1, std::numeric_limits<int32_t>::max(), 1);
			obs_property_int_set_suffix(p, " frames");
		}

		{ // Limits
			obs_properties_t* grp2 = obs_properties_create();
			obs_properties_add_group(grp, ST_I18N_RATECONTROL_LIMITS, D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS),
									 OBS_GROUP_NORMAL, grp2);

			{ // Bitrate
				auto p = obs_properties_add_int(grp2, ST_KEY_RATECONTROL_LIMITS_BITRATE,
												D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_BITRATE), 0,
												std::numeric_limits<int32_t>::max(), 1);
				obs_property_int_set_suffix(p, " kbit/s");
			}

			{ // Bitrate Under/Overshoot
				auto p1 = obs_properties_add_int_slider(grp2, ST_KEY_RATECONTROL_LIMITS_BITRATE_UNDERSHOOT,
														D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_BITRATE_UNDERSHOOT), -1,
														100, 1);
				auto p2 = obs_properties_add_int_slider(grp2, ST_KEY_RATECONTROL_LIMITS_BITRATE_OVERSHOOT,
														D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_BITRATE_OVERSHOOT), -1,
														100, 1);
				obs_property_float_set_suffix(p1, " %");
				obs_property_float_set_suffix(p2, " %");
			}

#ifdef AOM_CTRL_AOME_SET_CQ_LEVEL
			{ // Quality
				auto p = obs_properties_add_int_slider(grp2, ST_KEY_RATECONTROL_LIMITS_QUALITY,
													   D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_QUALITY), -1, 63, 1);
			}
#endif

			{ // Quantizer
				auto p1 =
					obs_properties_add_int_slider(grp2, ST_KEY_RATECONTROL_LIMITS_QUANTIZER_MINIMUM,
												  D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_QUANTIZER_MINIMUM), -1, 63, 1);
				auto p2 =
					obs_properties_add_int_slider(grp2, ST_KEY_RATECONTROL_LIMITS_QUANTIZER_MAXIMUM,
												  D_TRANSLATE(ST_I18N_RATECONTROL_LIMITS_QUANTIZER_MAXIMUM), -1, 63, 1);
			}
		}

		{ // Buffer
			obs_properties_t* grp2 = obs_properties_create();
			obs_properties_add_group(grp, ST_I18N_RATECONTROL_BUFFER, D_TRANSLATE(ST_I18N_RATECONTROL_BUFFER),
									 OBS_GROUP_NORMAL, grp2);

			{ // Buffer Size
				auto p = obs_properties_add_int(grp2, ST_KEY_RATECONTROL_BUFFER_SIZE,
												D_TRANSLATE(ST_I18N_RATECONTROL_BUFFER_SIZE), -1,
												std::numeric_limits<int32_t>::max(), 1);
				obs_property_int_set_suffix(p, " ms");
			}

			{ // Initial Buffer Size
				auto p = obs_properties_add_int(grp2, ST_KEY_RATECONTROL_BUFFER_SIZE_INITIAL,
												D_TRANSLATE(ST_I18N_RATECONTROL_BUFFER_SIZE_INITIAL), -1,
												std::numeric_limits<int32_t>::max(), 1);
				obs_property_int_set_suffix(p, " ms");
			}

			{ // Optimal Buffer Size
				auto p = obs_properties_add_int(grp2, ST_KEY_RATECONTROL_BUFFER_SIZE_OPTIMAL,
												D_TRANSLATE(ST_I18N_RATECONTROL_BUFFER_SIZE_OPTIMAL), -1,
												std::numeric_limits<int32_t>::max(), 1);
				obs_property_int_set_suffix(p, " ms");
			}
		}
	}

	{ // Key-Frame Options
		obs_properties_t* grp = obs_properties_create();
		obs_properties_add_group(props, ST_I18N_KEYFRAMES, D_TRANSLATE(ST_I18N_KEYFRAMES), OBS_GROUP_NORMAL, grp);

		{ // Key-Frame Interval Type
			auto p =
				obs_properties_add_list(grp, ST_KEY_KEYFRAMES_INTERVALTYPE, D_TRANSLATE(ST_I18N_KEYFRAMES_INTERVALTYPE),
										OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
			obs_property_set_modified_callback(p, modified_keyframes);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_KEYFRAMES_INTERVALTYPE_SECONDS), 0);
			obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_KEYFRAMES_INTERVALTYPE_FRAMES), 1);
		}

		{ // Key-Frame Interval Seconds
			auto p = obs_properties_add_float(grp, ST_KEY_KEYFRAMES_INTERVAL_SECONDS,
											  D_TRANSLATE(ST_I18N_KEYFRAMES_INTERVAL), 0.00,
											  std::numeric_limits<uint16_t>::max(), 0.01);
			obs_property_float_set_suffix(p, " seconds");
		}

		{ // Key-Frame Interval Frames
			auto p =
				obs_properties_add_int(grp, ST_KEY_KEYFRAMES_INTERVAL_FRAMES, D_TRANSLATE(ST_I18N_KEYFRAMES_INTERVAL),
									   0, std::numeric_limits<int32_t>::max(), 1);
			obs_property_int_set_suffix(p, " frames");
		}
	}

	{ // Advanced Options
		obs_properties_t* grp = obs_properties_create();
		obs_properties_add_group(props, ST_I18N_ADVANCED, D_TRANSLATE(ST_I18N_ADVANCED), OBS_GROUP_NORMAL, grp);

		{ // Threads
			auto p = obs_properties_add_int(grp, ST_KEY_ADVANCED_THREADS, D_TRANSLATE(ST_I18N_ADVANCED_THREADS), 0,
											std::numeric_limits<int32_t>::max(), 1);
		}

#ifdef AOM_CTRL_AV1E_SET_ROW_MT
		{ // Row-MT
			auto p = streamfx::util::obs_properties_add_tristate(grp, ST_KEY_ADVANCED_ROWMULTITHREADING,
																 D_TRANSLATE(ST_I18N_ADVANCED_ROWMULTITHREADING));
		}
#endif

#ifdef AOM_CTRL_AV1E_SET_TILE_COLUMNS
		{ // Tile Columns
			auto p = obs_properties_add_int_slider(grp, ST_KEY_ADVANCED_TILE_COLUMNS,
												   D_TRANSLATE(ST_I18N_ADVANCED_TILE_COLUMNS), -1, 6, 1);
		}
#endif

#ifdef AOM_CTRL_AV1E_SET_TILE_ROWS
		{ // Tile Rows
			auto p = obs_properties_add_int_slider(grp, ST_KEY_ADVANCED_TILE_ROWS,
												   D_TRANSLATE(ST_I18N_ADVANCED_TILE_ROWS), -1, 6, 1);
		}
#endif
		{
			obs_properties_t* grp2 = obs_properties_create();
			obs_properties_add_group(grp, ST_I18N_ADVANCED_TUNE, D_TRANSLATE(ST_I18N_ADVANCED_TUNE), OBS_GROUP_NORMAL,
									 grp2);

#ifdef AOM_CTRL_AOME_SET_TUNING
			{ // Tuning
				auto p = obs_properties_add_list(grp2, ST_KEY_ADVANCED_TUNE_METRIC,
												 D_TRANSLATE(ST_I18N_ADVANCED_TUNE_METRIC), OBS_COMBO_TYPE_LIST,
												 OBS_COMBO_FORMAT_INT);
				obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DEFAULT), -1);
				obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ADVANCED_TUNE_METRIC_PSNR),
										  static_cast<long long>(AOM_TUNE_PSNR));
				obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ADVANCED_TUNE_METRIC_SSIM),
										  static_cast<long long>(AOM_TUNE_SSIM));
#ifdef _DEBUG
				// These have no actual use outside of debug builds, way too slow!
				obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ADVANCED_TUNE_METRIC_VMAFWITHPREPROCESSING),
										  static_cast<long long>(AOM_TUNE_VMAF_WITH_PREPROCESSING));
				obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ADVANCED_TUNE_METRIC_VMAFWITHOUTPREPROCESSING),
										  static_cast<long long>(AOM_TUNE_VMAF_WITHOUT_PREPROCESSING));
				obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ADVANCED_TUNE_METRIC_VMAFMAXGAIN),
										  static_cast<long long>(AOM_TUNE_VMAF_MAX_GAIN));
				obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ADVANCED_TUNE_METRIC_VMAFNEGMAXGAIN),
										  static_cast<long long>(AOM_TUNE_VMAF_NEG_MAX_GAIN));
				obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ADVANCED_TUNE_METRIC_BUTTERAUGLI),
										  static_cast<long long>(AOM_TUNE_BUTTERAUGLI));
#endif
			}
#endif

#ifdef AOM_CTRL_AV1E_SET_TUNE_CONTENT
			{ // Content Tuning
				auto p = obs_properties_add_list(grp2, ST_KEY_ADVANCED_TUNE_CONTENT,
												 D_TRANSLATE(ST_I18N_ADVANCED_TUNE_CONTENT), OBS_COMBO_TYPE_LIST,
												 OBS_COMBO_FORMAT_INT);
				obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DEFAULT), static_cast<long long>(AOM_CONTENT_DEFAULT));
				obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ADVANCED_TUNE_CONTENT_SCREEN),
										  static_cast<long long>(AOM_CONTENT_SCREEN));
				obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_ADVANCED_TUNE_CONTENT_FILM),
										  static_cast<long long>(AOM_CONTENT_FILM));
			}
#endif
		}
	}

	return props;
}

#ifdef ENABLE_FRONTEND
bool aom_av1_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data)
{
	streamfx::open_url(HELP_URL);
	return false;
}
#endif
