408 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
			
		
		
	
	
			408 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
| // AUTOGENERATED COPYRIGHT HEADER START
 | |
| // Copyright (C) 2019-2023 Michael Fabian 'Xaymar' Dirks <info@xaymar.com>
 | |
| // Copyright (C) 2022 lainon <GermanAizek@yandex.ru>
 | |
| // AUTOGENERATED COPYRIGHT HEADER END
 | |
| 
 | |
| #include "source-mirror.hpp"
 | |
| #include "strings.hpp"
 | |
| #include <bitset>
 | |
| #include <cstring>
 | |
| #include <functional>
 | |
| #include <memory>
 | |
| #include <sstream>
 | |
| #include <stdexcept>
 | |
| #include <vector>
 | |
| #include "obs/gs/gs-helper.hpp"
 | |
| #include "obs/obs-source-tracker.hpp"
 | |
| #include "obs/obs-tools.hpp"
 | |
| #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 "<source::mirror> "
 | |
| #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
 | |
| 
 | |
| // OBS
 | |
| #ifdef _MSC_VER
 | |
| #pragma warning(push)
 | |
| #pragma warning(disable : 4464)
 | |
| #pragma warning(disable : 4820)
 | |
| #pragma warning(disable : 5220)
 | |
| #else
 | |
| #pragma GCC diagnostic push
 | |
| #pragma GCC diagnostic ignored "-Wall"
 | |
| #pragma GCC diagnostic ignored "-Wextra"
 | |
| #endif
 | |
| #include <media-io/audio-io.h>
 | |
| #ifdef _MSC_VER
 | |
| #pragma warning(pop)
 | |
| #else
 | |
| #pragma GCC diagnostic pop
 | |
| #endif
 | |
| 
 | |
| #define ST_I18N "Source.Mirror"
 | |
| #define ST_I18N_SOURCE ST_I18N ".Source"
 | |
| #define ST_KEY_SOURCE "Source.Mirror.Source"
 | |
| #define ST_I18N_SOURCE_AUDIO ST_I18N_SOURCE ".Audio"
 | |
| #define ST_KEY_SOURCE_AUDIO "Source.Mirror.Audio"
 | |
| #define ST_I18N_SOURCE_AUDIO_LAYOUT ST_I18N_SOURCE_AUDIO ".Layout"
 | |
| #define ST_KEY_SOURCE_AUDIO_LAYOUT "Source.Mirror.Audio.Layout"
 | |
| #define ST_I18N_SOURCE_AUDIO_LAYOUT_(x) ST_I18N_SOURCE_AUDIO_LAYOUT "." D_VSTR(x)
 | |
| 
 | |
| using namespace streamfx::source::mirror;
 | |
| 
 | |
| static constexpr std::string_view HELP_URL = "https://github.com/Xaymar/obs-StreamFX/wiki/Source-Mirror";
 | |
| 
 | |
| mirror_audio_data::mirror_audio_data(const audio_data* audio, speaker_layout layout)
 | |
| {
 | |
| 	// Build a clone of a packet.
 | |
| 	audio_t*                 oad = obs_get_audio();
 | |
| 	const audio_output_info* aoi = audio_output_get_info(oad);
 | |
| 	osa.frames                   = audio->frames;
 | |
| 	osa.timestamp                = audio->timestamp;
 | |
| 	osa.speakers                 = layout;
 | |
| 	osa.format                   = aoi->format;
 | |
| 	osa.samples_per_sec          = aoi->samples_per_sec;
 | |
| 	data.resize(MAX_AV_PLANES);
 | |
| 	for (std::size_t idx = 0; idx < MAX_AV_PLANES; idx++) {
 | |
| 		if (!audio->data[idx]) {
 | |
| 			osa.data[idx] = nullptr;
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		data[idx].resize(audio->frames * get_audio_bytes_per_channel(osa.format));
 | |
| 		memcpy(data[idx].data(), audio->data[idx], data[idx].size());
 | |
| 		osa.data[idx] = data[idx].data();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| mirror_instance::mirror_instance(obs_data_t* settings, obs_source_t* self) : obs::source_instance(settings, self), _source(), _source_child(), _signal_rename(), _audio_enabled(false), _audio_layout(SPEAKERS_UNKNOWN)
 | |
| {
 | |
| 	update(settings);
 | |
| }
 | |
| 
 | |
| mirror_instance::~mirror_instance()
 | |
| {
 | |
| 	release();
 | |
| }
 | |
| 
 | |
| uint32_t mirror_instance::get_width()
 | |
| {
 | |
| 	return _source_size.first ? _source_size.first : 1;
 | |
| }
 | |
| 
 | |
| uint32_t mirror_instance::get_height()
 | |
| {
 | |
| 	return _source_size.second ? _source_size.second : 1;
 | |
| }
 | |
| 
 | |
| void mirror_instance::load(obs_data_t* data)
 | |
| {
 | |
| 	update(data);
 | |
| }
 | |
| 
 | |
| void mirror_instance::migrate(obs_data_t* data, uint64_t version)
 | |
| {
 | |
| 	switch (version) {
 | |
| 	case 0:
 | |
| 		obs_data_set_int(data, ST_KEY_SOURCE_AUDIO_LAYOUT, obs_data_get_int(data, "Source.Mirror.Audio.Layout"));
 | |
| 		obs_data_unset_user_value(data, "Source.Mirror.Audio.Layout");
 | |
| 	case STREAMFX_VERSION:
 | |
| 		break;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void mirror_instance::update(obs_data_t* data)
 | |
| {
 | |
| 	// Audio
 | |
| 	_audio_enabled = obs_data_get_bool(data, ST_KEY_SOURCE_AUDIO);
 | |
| 	_audio_layout  = static_cast<speaker_layout>(obs_data_get_int(data, ST_KEY_SOURCE_AUDIO_LAYOUT));
 | |
| 
 | |
| 	// Acquire new source.
 | |
| 	acquire(obs_data_get_string(data, ST_KEY_SOURCE));
 | |
| }
 | |
| 
 | |
| void mirror_instance::save(obs_data_t* data)
 | |
| {
 | |
| 	if (_source) {
 | |
| 		obs_data_set_string(data, ST_KEY_SOURCE, obs_source_get_name(_source.get()));
 | |
| 	} else {
 | |
| 		obs_data_unset_user_value(data, ST_KEY_SOURCE);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void mirror_instance::video_tick(float_t time) {}
 | |
| 
 | |
| void mirror_instance::video_render(gs_effect_t* effect)
 | |
| {
 | |
| 	if (!_source)
 | |
| 		return;
 | |
| 	if ((obs_source_get_output_flags(_source.get()) & OBS_SOURCE_VIDEO) == 0)
 | |
| 		return;
 | |
| 
 | |
| #if defined(ENABLE_PROFILING) && !defined(D_PLATFORM_MAC) && _DEBUG
 | |
| 	streamfx::obs::gs::debug_marker gdmp{streamfx::obs::gs::debug_color_source, "Source Mirror '%s' for '%s'", obs_source_get_name(_self), obs_source_get_name(_source.get())};
 | |
| #endif
 | |
| 
 | |
| 	_source_size.first  = obs_source_get_width(_source.get());
 | |
| 	_source_size.second = obs_source_get_height(_source.get());
 | |
| 
 | |
| 	obs_source_video_render(_source.get());
 | |
| }
 | |
| 
 | |
| void mirror_instance::enum_active_sources(obs_source_enum_proc_t cb, void* ptr)
 | |
| {
 | |
| 	if (!_source)
 | |
| 		return;
 | |
| 	cb(_self, _source.get(), ptr);
 | |
| }
 | |
| 
 | |
| void mirror_instance::enum_all_sources(obs_source_enum_proc_t cb, void* ptr)
 | |
| {
 | |
| 	if (!_source)
 | |
| 		return;
 | |
| 
 | |
| 	cb(_self, _source.get(), ptr);
 | |
| }
 | |
| 
 | |
| void mirror_instance::acquire(std::string source_name)
 | |
| {
 | |
| 	try {
 | |
| 		release();
 | |
| 
 | |
| 		// Find source by name if possible.
 | |
| 		decltype(_source) source{source_name};
 | |
| 		if ((!source) || (source == _self)) { // If we failed, just exit early.
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Everything went well, store.
 | |
| 		_source_child       = std::make_shared<::streamfx::obs::source_active_child>(_self, source);
 | |
| 		_source             = std::move(source);
 | |
| 		_source_size.first  = obs_source_get_width(_source);
 | |
| 		_source_size.second = obs_source_get_height(_source);
 | |
| 
 | |
| 		// Listen to any audio the source spews out.
 | |
| 		if (_audio_enabled) {
 | |
| 			_signal_audio = std::make_shared<obs::audio_signal_handler>(_source);
 | |
| 			_signal_audio->event.add(std::bind(&mirror_instance::on_audio, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
 | |
| 		}
 | |
| 	} catch (...) {
 | |
| 		release();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void mirror_instance::release()
 | |
| {
 | |
| 	_signal_audio.reset();
 | |
| 	_signal_rename.reset();
 | |
| 	_source_child.reset();
 | |
| 	_source.release();
 | |
| }
 | |
| 
 | |
| void mirror_instance::on_audio(::streamfx::obs::source, const audio_data* audio, bool)
 | |
| {
 | |
| 	// Immediately quit if there isn't any actual audio to send out.
 | |
| 	if (!_audio_enabled) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	// Detect Audio Layout from underlying audio.
 | |
| 	speaker_layout detected_layout;
 | |
| 	if (_audio_layout != SPEAKERS_UNKNOWN) {
 | |
| 		detected_layout = _audio_layout;
 | |
| 	} else {
 | |
| 		std::bitset<MAX_AV_PLANES> layout_detection;
 | |
| 		for (std::size_t idx = 0; idx < MAX_AV_PLANES; idx++) {
 | |
| 			layout_detection.set(idx, audio->data[idx] != nullptr);
 | |
| 		}
 | |
| 		switch (layout_detection.to_ulong()) {
 | |
| 		case 0b00000001:
 | |
| 			detected_layout = SPEAKERS_MONO;
 | |
| 			break;
 | |
| 		case 0b00000011:
 | |
| 			detected_layout = SPEAKERS_STEREO;
 | |
| 			break;
 | |
| 		case 0b00000111:
 | |
| 			detected_layout = SPEAKERS_2POINT1;
 | |
| 			break;
 | |
| 		case 0b00001111:
 | |
| 			detected_layout = SPEAKERS_4POINT0;
 | |
| 			break;
 | |
| 		case 0b00011111:
 | |
| 			detected_layout = SPEAKERS_4POINT1;
 | |
| 			break;
 | |
| 		case 0b00111111:
 | |
| 			detected_layout = SPEAKERS_5POINT1;
 | |
| 			break;
 | |
| 		case 0b11111111:
 | |
| 			detected_layout = SPEAKERS_7POINT1;
 | |
| 			break;
 | |
| 		default:
 | |
| 			detected_layout = SPEAKERS_UNKNOWN;
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	{
 | |
| 		std::unique_lock<std::mutex> ul(_audio_queue_lock);
 | |
| 		_audio_queue.emplace(audio, detected_layout);
 | |
| 	}
 | |
| 
 | |
| 	// Create a clone of the audio data and push it to the thread pool.
 | |
| 	streamfx::threadpool()->push(std::bind(&mirror_instance::audio_output, this, std::placeholders::_1), nullptr);
 | |
| }
 | |
| 
 | |
| void mirror_instance::audio_output(std::shared_ptr<void> data)
 | |
| {
 | |
| 	std::unique_lock<std::mutex> ul(_audio_queue_lock);
 | |
| 	while (_audio_queue.size() > 0) {
 | |
| 		obs_source_output_audio(_self, &((_audio_queue.front()).osa));
 | |
| 		_audio_queue.pop();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| mirror_factory::mirror_factory()
 | |
| {
 | |
| 	_info.id           = S_PREFIX "source-mirror";
 | |
| 	_info.type         = OBS_SOURCE_TYPE_INPUT;
 | |
| 	_info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_AUDIO;
 | |
| 
 | |
| 	support_active_child_sources(true);
 | |
| 	support_child_sources(true);
 | |
| 	finish_setup();
 | |
| 	register_proxy("obs-stream-effects-source-mirror");
 | |
| }
 | |
| 
 | |
| mirror_factory::~mirror_factory() {}
 | |
| 
 | |
| const char* mirror_factory::get_name()
 | |
| {
 | |
| 	return D_TRANSLATE(ST_I18N);
 | |
| }
 | |
| 
 | |
| void mirror_factory::get_defaults2(obs_data_t* data)
 | |
| {
 | |
| 	obs_data_set_default_string(data, ST_KEY_SOURCE, "");
 | |
| 	obs_data_set_default_bool(data, ST_KEY_SOURCE_AUDIO, false);
 | |
| 	obs_data_set_default_int(data, ST_KEY_SOURCE_AUDIO_LAYOUT, static_cast<int64_t>(SPEAKERS_UNKNOWN));
 | |
| }
 | |
| 
 | |
| static bool modified_properties(obs_properties_t* pr, obs_property_t* p, obs_data_t* data) noexcept
 | |
| {
 | |
| 	try {
 | |
| 		if (obs_properties_get(pr, ST_KEY_SOURCE_AUDIO) == p) {
 | |
| 			bool show = obs_data_get_bool(data, ST_KEY_SOURCE_AUDIO);
 | |
| 			obs_property_set_visible(obs_properties_get(pr, ST_KEY_SOURCE_AUDIO_LAYOUT), show);
 | |
| 			return true;
 | |
| 		}
 | |
| 		return false;
 | |
| 	} catch (...) {
 | |
| 		return false;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| obs_properties_t* mirror_factory::get_properties2(mirror_instance* data)
 | |
| {
 | |
| 	obs_properties_t* pr = obs_properties_create();
 | |
| 	obs_property_t*   p  = nullptr;
 | |
| 
 | |
| #ifdef ENABLE_FRONTEND
 | |
| 	{
 | |
| 		obs_properties_add_button2(pr, S_MANUAL_OPEN, D_TRANSLATE(S_MANUAL_OPEN), streamfx::source::mirror::mirror_factory::on_manual_open, nullptr);
 | |
| 	}
 | |
| #endif
 | |
| 
 | |
| 	{
 | |
| 		p = obs_properties_add_list(pr, ST_KEY_SOURCE, D_TRANSLATE(ST_I18N_SOURCE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
 | |
| 		obs_property_set_modified_callback(p, modified_properties);
 | |
| 
 | |
| 		obs_property_list_add_string(p, "", "");
 | |
| 		obs::source_tracker::instance()->enumerate(
 | |
| 			[&p](std::string name, ::streamfx::obs::source) {
 | |
| 				std::stringstream sstr;
 | |
| 				sstr << name << " (" << D_TRANSLATE(S_SOURCETYPE_SOURCE) << ")";
 | |
| 				obs_property_list_add_string(p, sstr.str().c_str(), name.c_str());
 | |
| 				return false;
 | |
| 			},
 | |
| 			obs::source_tracker::filter_sources);
 | |
| 		obs::source_tracker::instance()->enumerate(
 | |
| 			[&p](std::string name, ::streamfx::obs::source) {
 | |
| 				std::stringstream sstr;
 | |
| 				sstr << name << " (" << D_TRANSLATE(S_SOURCETYPE_SCENE) << ")";
 | |
| 				obs_property_list_add_string(p, sstr.str().c_str(), name.c_str());
 | |
| 				return false;
 | |
| 			},
 | |
| 			obs::source_tracker::filter_scenes);
 | |
| 	}
 | |
| 
 | |
| 	{
 | |
| 		p = obs_properties_add_bool(pr, ST_KEY_SOURCE_AUDIO, D_TRANSLATE(ST_I18N_SOURCE_AUDIO));
 | |
| 		obs_property_set_modified_callback(p, modified_properties);
 | |
| 	}
 | |
| 
 | |
| 	{
 | |
| 		p = obs_properties_add_list(pr, ST_KEY_SOURCE_AUDIO_LAYOUT, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
 | |
| 		obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Unknown)), static_cast<int64_t>(SPEAKERS_UNKNOWN));
 | |
| 		obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Mono)), static_cast<int64_t>(SPEAKERS_MONO));
 | |
| 		obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Stereo)), static_cast<int64_t>(SPEAKERS_STEREO));
 | |
| 		obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(StereoLFE)), static_cast<int64_t>(SPEAKERS_2POINT1));
 | |
| 		obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Quadraphonic)), static_cast<int64_t>(SPEAKERS_4POINT0));
 | |
| 		obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(QuadraphonicLFE)), static_cast<int64_t>(SPEAKERS_4POINT1));
 | |
| 		obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Surround)), static_cast<int64_t>(SPEAKERS_5POINT1));
 | |
| 		obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(FullSurround)), static_cast<int64_t>(SPEAKERS_7POINT1));
 | |
| 	}
 | |
| 
 | |
| 	return pr;
 | |
| }
 | |
| 
 | |
| #ifdef ENABLE_FRONTEND
 | |
| bool mirror_factory::on_manual_open(obs_properties_t* props, obs_property_t* property, void* data)
 | |
| {
 | |
| 	try {
 | |
| 		streamfx::open_url(HELP_URL);
 | |
| 		return false;
 | |
| 	} catch (const std::exception& ex) {
 | |
| 		D_LOG_ERROR("Failed to open manual due to error: %s", ex.what());
 | |
| 		return false;
 | |
| 	} catch (...) {
 | |
| 		D_LOG_ERROR("Failed to open manual due to unknown error.", "");
 | |
| 		return false;
 | |
| 	}
 | |
| }
 | |
| #endif
 | |
| 
 | |
| std::shared_ptr<mirror_factory> mirror_factory::instance()
 | |
| {
 | |
| 	static std::weak_ptr<mirror_factory> winst;
 | |
| 	static std::mutex                         mtx;
 | |
| 
 | |
| 	std::unique_lock<decltype(mtx)> lock(mtx);
 | |
| 	auto                            instance = winst.lock();
 | |
| 	if (!instance) {
 | |
| 		instance = std::shared_ptr<mirror_factory>(new mirror_factory());
 | |
| 		winst    = instance;
 | |
| 	}
 | |
| 	return instance;
 | |
| }
 | |
| 
 | |
| static std::shared_ptr<mirror_factory> loader_instance;
 | |
| 
 | |
| static auto loader = streamfx::loader(
 | |
| 	[]() { // Initalizer
 | |
| 		loader_instance = mirror_factory::instance();
 | |
| 	},
 | |
| 	[]() { // Finalizer
 | |
| 		loader_instance.reset();
 | |
| 	},
 | |
| 	streamfx::loader_priority::NORMAL);
 |