// AUTOGENERATED COPYRIGHT HEADER START // Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END #include "source-mirror.hpp" #include "strings.hpp" #include #include #include #include #include #include #include #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 " " #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 #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(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 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(_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 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 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 data) { std::unique_lock 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(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(SPEAKERS_UNKNOWN)); obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Mono)), static_cast(SPEAKERS_MONO)); obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Stereo)), static_cast(SPEAKERS_STEREO)); obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(StereoLFE)), static_cast(SPEAKERS_2POINT1)); obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Quadraphonic)), static_cast(SPEAKERS_4POINT0)); obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(QuadraphonicLFE)), static_cast(SPEAKERS_4POINT1)); obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(Surround)), static_cast(SPEAKERS_5POINT1)); obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_SOURCE_AUDIO_LAYOUT_(FullSurround)), static_cast(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::instance() { static std::weak_ptr winst; static std::mutex mtx; std::unique_lock lock(mtx); auto instance = winst.lock(); if (!instance) { instance = std::shared_ptr(new mirror_factory()); winst = instance; } return instance; } static std::shared_ptr loader_instance; static auto loader = streamfx::loader( []() { // Initalizer loader_instance = mirror_factory::instance(); }, []() { // Finalizer loader_instance.reset(); }, streamfx::loader_priority::NORMAL);