diff --git a/CMakeLists.txt b/CMakeLists.txt index 1802b6a7..bbb89f2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -229,6 +229,7 @@ set(advanced-scene-switcher_HEADERS src/headers/macro-condition-idle.hpp src/headers/macro-condition-macro.hpp src/headers/macro-condition-media.hpp + src/headers/macro-condition-obs-stats.hpp src/headers/macro-condition-plugin-state.hpp src/headers/macro-condition-process.hpp src/headers/macro-condition-recording.hpp @@ -330,6 +331,7 @@ set(advanced-scene-switcher_SOURCES src/macro-condition-idle.cpp src/macro-condition-macro.cpp src/macro-condition-media.cpp + src/macro-condition-obs-stats.cpp src/macro-condition-plugin-state.cpp src/macro-condition-process.cpp src/macro-condition-recording.cpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index d57ec074..e0eae992 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -291,6 +291,25 @@ AdvSceneSwitcher.condition.errorStatus="OpenVR error: " AdvSceneSwitcher.condition.openvr.entry.line1="HMD is in ..." AdvSceneSwitcher.condition.openvr.entry.line2="{{controls}}" AdvSceneSwitcher.condition.openvr.entry.line3="HMD is currently at {{xPos}} x {{yPos}} x {{zPos}}" +AdvSceneSwitcher.condition.stats="OBS stats" +AdvSceneSwitcher.condition.stats.type.fps="FPS" +AdvSceneSwitcher.condition.stats.type.CPUUsage="CPU Usage" +AdvSceneSwitcher.condition.stats.type.HDDSpaceAvailable="Disk space available" +AdvSceneSwitcher.condition.stats.type.memoryUsage="Memory Usage" +AdvSceneSwitcher.condition.stats.type.averageTimeToRender="Average time to render frame" +AdvSceneSwitcher.condition.stats.type.skippedFrames="Skipped frames due to encoding lag" +AdvSceneSwitcher.condition.stats.type.missedFrames="Frames missed due to rendering lag" +AdvSceneSwitcher.condition.stats.type.droppedFrames.stream="Stream dropped frames" +AdvSceneSwitcher.condition.stats.type.megabytesSent.stream="Stream total data output" +AdvSceneSwitcher.condition.stats.type.bitrate.stream="Stream bitrate" +AdvSceneSwitcher.condition.stats.type.droppedFrames.recording="Recording dropped frames" +AdvSceneSwitcher.condition.stats.type.megabytesSent.recording="Recording total data output" +AdvSceneSwitcher.condition.stats.type.bitrate.recording="Recording bitrate" +AdvSceneSwitcher.condition.stats.condition.above="above" +AdvSceneSwitcher.condition.stats.condition.equals="equal to" +AdvSceneSwitcher.condition.stats.condition.below="below" +AdvSceneSwitcher.condition.stats.dockHint="You can open the \"Stats\" dock to view the current status" +AdvSceneSwitcher.condition.stats.entry="{{stats}} is {{condition}} {{value}}" ; Macro Actions AdvSceneSwitcher.action.switchScene="Switch scene" diff --git a/src/headers/macro-condition-obs-stats.hpp b/src/headers/macro-condition-obs-stats.hpp new file mode 100644 index 00000000..94ce277f --- /dev/null +++ b/src/headers/macro-condition-obs-stats.hpp @@ -0,0 +1,118 @@ +#pragma once +#include "macro.hpp" +#include +#include +#include +#include + +enum class StatsType { + FPS, + CPU_USAGE, + DISK_USAGE, // not implemented + MEM_USAGE, + AVG_FRAMETIME, + RENDER_LAG, + ENCODE_LAG, + STREAM_DROPPED_FRAMES, + STREAM_BITRATE, + STREAM_MB_SENT, + RECORDING_DROPPED_FRAMES, // not sure if this makes sense + RECORDING_BITRATE, + RECORDING_MB_SENT, +}; + +enum class StatsCondition { + ABOVE, + EQUALS, + BELOW, +}; + +class MacroConditionStats : public MacroCondition { +public: + MacroConditionStats(Macro *m); + ~MacroConditionStats(); + bool CheckCondition(); + bool Save(obs_data_t *obj); + bool Load(obs_data_t *obj); + std::string GetShortDesc(); + std::string GetId() { return id; }; + static std::shared_ptr Create(Macro *m) + { + return std::make_shared(m); + } + + double _value = 0; + StatsType _type = StatsType::FPS; + StatsCondition _condition = StatsCondition::ABOVE; + +private: + bool CheckFPS(); + bool CheckCPU(); + bool CheckMemory(); + bool CheckAvgFrametime(); + bool CheckRenderLag(); + bool CheckEncodeLag(); + bool CheckStreamDroppedFrames(); + bool CheckStreamBitrate(); + bool CheckStreamMBSent(); + bool CheckRecordingDroppedFrames(); + bool CheckRecordingBitrate(); + bool CheckRecordingMBSent(); + + os_cpu_usage_info_t *_cpu_info = nullptr; + uint32_t _first_encoded = 0xFFFFFFFF; + uint32_t _first_skipped = 0xFFFFFFFF; + uint32_t _first_rendered = 0xFFFFFFFF; + uint32_t _first_lagged = 0xFFFFFFFF; + struct OutputInfo { + void Update(obs_output_t *output); + + uint64_t lastBytesSent = 0; + uint64_t lastBytesSentTime = 0; + int first_total = 0; + int first_dropped = 0; + double dropped_relative = 0.0; + long double kbps = 0.0l; + }; + OutputInfo _streamInfo; + OutputInfo _recordingInfo; + + static bool _registered; + static const std::string id; +}; + +class MacroConditionStatsEdit : public QWidget { + Q_OBJECT + +public: + MacroConditionStatsEdit( + QWidget *parent, + std::shared_ptr cond = nullptr); + void UpdateEntryData(); + static QWidget *Create(QWidget *parent, + std::shared_ptr cond) + { + return new MacroConditionStatsEdit( + parent, + std::dynamic_pointer_cast(cond)); + } + +private slots: + void ValueChanged(double value); + void StatsTypeChanged(int type); + void ConditionChanged(int cond); + +signals: + void HeaderInfoChanged(const QString &); + +protected: + QComboBox *_stats; + QComboBox *_condition; + QDoubleSpinBox *_value; + std::shared_ptr _entryData; + +private: + void SetWidgetVisibility(); + + bool _loading = true; +}; diff --git a/src/macro-condition-obs-stats.cpp b/src/macro-condition-obs-stats.cpp new file mode 100644 index 00000000..401723ae --- /dev/null +++ b/src/macro-condition-obs-stats.cpp @@ -0,0 +1,584 @@ +#include "headers/macro-condition-edit.hpp" +#include "headers/macro-condition-obs-stats.hpp" +#include "headers/utility.hpp" +#include "headers/advanced-scene-switcher.hpp" + +const std::string MacroConditionStats::id = "obs_stats"; + +bool MacroConditionStats::_registered = MacroConditionFactory::Register( + MacroConditionStats::id, + {MacroConditionStats::Create, MacroConditionStatsEdit::Create, + "AdvSceneSwitcher.condition.stats"}); + +static std::map statsTypes = { + {StatsType::FPS, "AdvSceneSwitcher.condition.stats.type.fps"}, + {StatsType::CPU_USAGE, + "AdvSceneSwitcher.condition.stats.type.CPUUsage"}, + {StatsType::DISK_USAGE, + "AdvSceneSwitcher.condition.stats.type.HDDSpaceAvailable"}, + {StatsType::MEM_USAGE, + "AdvSceneSwitcher.condition.stats.type.memoryUsage"}, + {StatsType::AVG_FRAMETIME, + "AdvSceneSwitcher.condition.stats.type.averageTimeToRender"}, + {StatsType::RENDER_LAG, + "AdvSceneSwitcher.condition.stats.type.missedFrames"}, + {StatsType::ENCODE_LAG, + "AdvSceneSwitcher.condition.stats.type.skippedFrames"}, + {StatsType::STREAM_DROPPED_FRAMES, + "AdvSceneSwitcher.condition.stats.type.droppedFrames.stream"}, + {StatsType::STREAM_BITRATE, + "AdvSceneSwitcher.condition.stats.type.bitrate.stream"}, + {StatsType::STREAM_MB_SENT, + "AdvSceneSwitcher.condition.stats.type.megabytesSent.stream"}, + {StatsType::RECORDING_DROPPED_FRAMES, + "AdvSceneSwitcher.condition.stats.type.droppedFrames.recording"}, + {StatsType::RECORDING_BITRATE, + "AdvSceneSwitcher.condition.stats.type.bitrate.recording"}, + {StatsType::RECORDING_MB_SENT, + "AdvSceneSwitcher.condition.stats.type.megabytesSent.recording"}, +}; + +static std::map statsConditionTypes = { + {StatsCondition::ABOVE, + "AdvSceneSwitcher.condition.stats.condition.above"}, + {StatsCondition::EQUALS, + "AdvSceneSwitcher.condition.stats.condition.equals"}, + {StatsCondition::BELOW, + "AdvSceneSwitcher.condition.stats.condition.below"}, +}; + +MacroConditionStats::MacroConditionStats(Macro *m) + : MacroCondition(m), _cpu_info(os_cpu_usage_info_start()) +{ +} + +MacroConditionStats::~MacroConditionStats() +{ + os_cpu_usage_info_destroy(_cpu_info); +} + +bool doubleEquals(double left, double right, double epsilon) +{ + return (fabs(left - right) < epsilon); +} + +bool MacroConditionStats::CheckFPS() +{ + switch (_condition) { + case StatsCondition::ABOVE: + return obs_get_active_fps() > _value; + case StatsCondition::EQUALS: + return doubleEquals(obs_get_active_fps(), _value, 0.01); + case StatsCondition::BELOW: + return obs_get_active_fps() < _value; + default: + break; + } + return false; +} + +bool MacroConditionStats::CheckCPU() +{ + double usage = os_cpu_usage_info_query(_cpu_info); + + switch (_condition) { + case StatsCondition::ABOVE: + return usage > _value; + case StatsCondition::EQUALS: + return doubleEquals(usage, _value, 0.1); + case StatsCondition::BELOW: + return usage < _value; + default: + break; + } + return false; +} + +bool MacroConditionStats::CheckMemory() +{ + auto rss = + (long double)os_get_proc_resident_size() / (1024.0l * 1024.0l); + + switch (_condition) { + case StatsCondition::ABOVE: + return rss > _value; + case StatsCondition::EQUALS: + return doubleEquals(rss, _value, 0.1); + case StatsCondition::BELOW: + return rss < _value; + default: + break; + } + return false; +} + +bool MacroConditionStats::CheckAvgFrametime() +{ + auto num = (long double)obs_get_average_frame_time_ns() / 1000000.0l; + + switch (_condition) { + case StatsCondition::ABOVE: + return num > _value; + case StatsCondition::EQUALS: + return doubleEquals(num, _value, 0.1); + case StatsCondition::BELOW: + return num < _value; + default: + break; + } + return false; +} + +bool MacroConditionStats::CheckRenderLag() +{ + uint32_t total_rendered = obs_get_total_frames(); + uint32_t total_lagged = obs_get_lagged_frames(); + + if (total_rendered < _first_rendered || total_lagged < _first_lagged) { + _first_rendered = total_rendered; + _first_lagged = total_lagged; + } + total_rendered -= _first_rendered; + total_lagged -= _first_lagged; + + auto num = total_rendered ? (long double)total_lagged / + (long double)total_rendered + : 0.0l; + num *= 100.0l; + + switch (_condition) { + case StatsCondition::ABOVE: + return num > _value; + case StatsCondition::EQUALS: + return doubleEquals(num, _value, 0.1); + case StatsCondition::BELOW: + return num < _value; + default: + break; + } + return false; +} + +bool MacroConditionStats::CheckEncodeLag() +{ + video_t *video = obs_get_video(); + uint32_t total_encoded = video_output_get_total_frames(video); + uint32_t total_skipped = video_output_get_skipped_frames(video); + + if (total_encoded < _first_encoded || total_skipped < _first_encoded) { + _first_encoded = total_encoded; + _first_skipped = total_skipped; + } + total_encoded -= _first_encoded; + total_skipped -= _first_skipped; + + auto num = total_encoded ? (long double)total_skipped / + (long double)total_encoded + : 0.0l; + num *= 100.0l; + + switch (_condition) { + case StatsCondition::ABOVE: + return num > _value; + case StatsCondition::EQUALS: + return doubleEquals(num, _value, 0.1); + case StatsCondition::BELOW: + return num < _value; + default: + break; + } + return false; +} + +void MacroConditionStats::OutputInfo::Update(obs_output_t *output) +{ + uint64_t totalBytes = output ? obs_output_get_total_bytes(output) : 0; + uint64_t curTime = os_gettime_ns(); + uint64_t bytesSent = totalBytes; + + if (bytesSent < lastBytesSent) + bytesSent = 0; + if (bytesSent == 0) + lastBytesSent = 0; + + uint64_t bitsBetween = (bytesSent - lastBytesSent) * 8; + long double timePassed = + (long double)(curTime - lastBytesSentTime) / 1000000000.0l; + kbps = (long double)bitsBetween / timePassed / 1000.0l; + + if (timePassed < 0.01l) + kbps = 0.0l; + + int total = output ? obs_output_get_total_frames(output) : 0; + int dropped = output ? obs_output_get_frames_dropped(output) : 0; + + if (total < first_total || dropped < first_dropped) { + first_total = 0; + first_dropped = 0; + } + + total -= first_total; + dropped -= first_dropped; + + dropped_relative = + total ? (long double)dropped / (long double)total * 100.0l + : 0.0l; + + lastBytesSent = bytesSent; + lastBytesSentTime = curTime; +} + +bool MacroConditionStats::CheckStreamDroppedFrames() +{ + auto output = obs_frontend_get_streaming_output(); + _streamInfo.Update(output); + obs_output_release(output); + + switch (_condition) { + case StatsCondition::ABOVE: + return _streamInfo.dropped_relative > _value; + case StatsCondition::EQUALS: + return doubleEquals(_streamInfo.dropped_relative, _value, 0.1); + case StatsCondition::BELOW: + return _streamInfo.dropped_relative < _value; + default: + break; + } + return false; +} + +bool MacroConditionStats::CheckStreamBitrate() +{ + auto output = obs_frontend_get_streaming_output(); + _streamInfo.Update(output); + obs_output_release(output); + + switch (_condition) { + case StatsCondition::ABOVE: + return _streamInfo.kbps > _value; + case StatsCondition::EQUALS: + return doubleEquals(_streamInfo.kbps, _value, 1.0); + case StatsCondition::BELOW: + return _streamInfo.kbps < _value; + default: + break; + } + return false; +} + +bool MacroConditionStats::CheckStreamMBSent() +{ + auto output = obs_frontend_get_streaming_output(); + uint64_t totalBytes = output ? obs_output_get_total_bytes(output) : 0; + obs_output_release(output); + long double num = (long double)totalBytes / (1024.0l * 1024.0l); + + switch (_condition) { + case StatsCondition::ABOVE: + return num > _value; + case StatsCondition::EQUALS: + return doubleEquals(num, _value, 0.1); + case StatsCondition::BELOW: + return num < _value; + default: + break; + } + return false; +} + +bool MacroConditionStats::CheckRecordingDroppedFrames() +{ + auto output = obs_frontend_get_recording_output(); + _recordingInfo.Update(output); + obs_output_release(output); + + switch (_condition) { + case StatsCondition::ABOVE: + return _recordingInfo.dropped_relative > _value; + case StatsCondition::EQUALS: + return doubleEquals(_recordingInfo.dropped_relative, _value, + 0.1); + case StatsCondition::BELOW: + return _recordingInfo.dropped_relative < _value; + default: + break; + } + return false; +} + +bool MacroConditionStats::CheckRecordingBitrate() +{ + auto output = obs_frontend_get_recording_output(); + _recordingInfo.Update(output); + obs_output_release(output); + + switch (_condition) { + case StatsCondition::ABOVE: + return _recordingInfo.kbps > _value; + case StatsCondition::EQUALS: + return doubleEquals(_recordingInfo.kbps, _value, 1.0); + case StatsCondition::BELOW: + return _recordingInfo.kbps < _value; + default: + break; + } + return false; +} + +bool MacroConditionStats::CheckRecordingMBSent() +{ + auto output = obs_frontend_get_recording_output(); + uint64_t totalBytes = output ? obs_output_get_total_bytes(output) : 0; + obs_output_release(output); + long double num = (long double)totalBytes / (1024.0l * 1024.0l); + + switch (_condition) { + case StatsCondition::ABOVE: + return num > _value; + case StatsCondition::EQUALS: + return doubleEquals(num, _value, 0.1); + case StatsCondition::BELOW: + return num < _value; + default: + break; + } + return false; +} + +bool MacroConditionStats::CheckCondition() +{ + switch (_type) { + case StatsType::FPS: + return CheckFPS(); + case StatsType::CPU_USAGE: + return CheckCPU(); + case StatsType::DISK_USAGE: + // TODO: not implemented + break; + case StatsType::MEM_USAGE: + return CheckMemory(); + case StatsType::AVG_FRAMETIME: + return CheckAvgFrametime(); + case StatsType::RENDER_LAG: + return CheckRenderLag(); + case StatsType::ENCODE_LAG: + return CheckEncodeLag(); + case StatsType::STREAM_DROPPED_FRAMES: + return CheckStreamDroppedFrames(); + case StatsType::STREAM_BITRATE: + return CheckStreamBitrate(); + case StatsType::STREAM_MB_SENT: + return CheckStreamMBSent(); + case StatsType::RECORDING_DROPPED_FRAMES: + return CheckRecordingDroppedFrames(); + case StatsType::RECORDING_BITRATE: + return CheckRecordingBitrate(); + case StatsType::RECORDING_MB_SENT: + return CheckRecordingMBSent(); + default: + break; + } + + return false; +} + +bool MacroConditionStats::Save(obs_data_t *obj) +{ + MacroCondition::Save(obj); + obs_data_set_double(obj, "value", _value); + obs_data_set_int(obj, "type", static_cast(_type)); + obs_data_set_int(obj, "condition", static_cast(_condition)); + return true; +} + +bool MacroConditionStats::Load(obs_data_t *obj) +{ + + MacroCondition::Load(obj); + _value = obs_data_get_double(obj, "value"); + _type = static_cast(obs_data_get_int(obj, "type")); + _condition = + static_cast(obs_data_get_int(obj, "condition")); + return true; +} + +std::string MacroConditionStats::GetShortDesc() +{ + auto it = statsTypes.find(_type); + if (it != statsTypes.end()) { + return obs_module_text(it->second.c_str()); + } + return ""; +} + +static inline void populateStatsTypes(QComboBox *list) +{ + list->clear(); + for (auto entry : statsTypes) { + list->addItem(obs_module_text(entry.second.c_str())); + // TODO: not implemented + if (entry.first == StatsType::DISK_USAGE) { + qobject_cast(list->view()) + ->setRowHidden(list->count() - 1, true); + } + } +} + +static inline void populateConditionSelection(QComboBox *list) +{ + list->clear(); + for (auto entry : statsConditionTypes) { + list->addItem(obs_module_text(entry.second.c_str())); + } +} + +MacroConditionStatsEdit::MacroConditionStatsEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _stats(new QComboBox), + _condition(new QComboBox), + _value(new QDoubleSpinBox) +{ + _value->setMaximum(999999999999); + + populateStatsTypes(_stats); + populateConditionSelection(_condition); + + setToolTip( + obs_module_text("AdvSceneSwitcher.condition.stats.dockHint")); + + QWidget::connect(_value, SIGNAL(valueChanged(double)), this, + SLOT(ValueChanged(double))); + QWidget::connect(_stats, SIGNAL(currentIndexChanged(int)), this, + SLOT(StatsTypeChanged(int))); + QWidget::connect(_condition, SIGNAL(currentIndexChanged(int)), this, + SLOT(ConditionChanged(int))); + + QHBoxLayout *layout = new QHBoxLayout; + std::unordered_map widgetPlaceholders = { + {"{{value}}", _value}, + {"{{stats}}", _stats}, + {"{{condition}}", _condition}, + }; + placeWidgets(obs_module_text("AdvSceneSwitcher.condition.stats.entry"), + layout, widgetPlaceholders); + setLayout(layout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionStatsEdit::ValueChanged(double value) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_value = value; +} + +void MacroConditionStatsEdit::StatsTypeChanged(int type) +{ + if (_loading || !_entryData) { + return; + } + + { + std::lock_guard lock(switcher->m); + _entryData->_type = static_cast(type); + SetWidgetVisibility(); + } + _value->setValue(0); + emit HeaderInfoChanged( + QString::fromStdString(_entryData->GetShortDesc())); +} + +void MacroConditionStatsEdit::ConditionChanged(int cond) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_condition = static_cast(cond); +} + +void MacroConditionStatsEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _value->setValue(_entryData->_value); + _stats->setCurrentIndex(static_cast(_entryData->_type)); + _condition->setCurrentIndex(static_cast(_entryData->_condition)); + SetWidgetVisibility(); +} + +void MacroConditionStatsEdit::SetWidgetVisibility() +{ + if (!_entryData) { + return; + } + + switch (_entryData->_type) { + case StatsType::FPS: + _value->setMaximum(1000); + _value->setSuffix(""); + break; + case StatsType::CPU_USAGE: + _value->setMaximum(100); + _value->setSuffix("%"); + break; + case StatsType::DISK_USAGE: + _value->setMaximum(999999999999); + _value->setSuffix("MB"); + break; + case StatsType::MEM_USAGE: + _value->setMaximum(999999999999); + _value->setSuffix("MB"); + break; + case StatsType::AVG_FRAMETIME: + _value->setMaximum(999999999999); + _value->setSuffix("ms"); + break; + case StatsType::RENDER_LAG: + _value->setMaximum(100); + _value->setSuffix("%"); + break; + case StatsType::ENCODE_LAG: + _value->setMaximum(100); + _value->setSuffix("%"); + break; + case StatsType::STREAM_DROPPED_FRAMES: + _value->setMaximum(100); + _value->setSuffix("%"); + break; + case StatsType::STREAM_BITRATE: + _value->setMaximum(999999999999); + _value->setSuffix("kb/s"); + break; + case StatsType::STREAM_MB_SENT: + _value->setMaximum(999999999999); + _value->setSuffix("MB"); + break; + case StatsType::RECORDING_DROPPED_FRAMES: + _value->setMaximum(100); + _value->setSuffix("%"); + break; + case StatsType::RECORDING_BITRATE: + _value->setMaximum(999999999999); + _value->setSuffix("kb/s"); + break; + case StatsType::RECORDING_MB_SENT: + _value->setMaximum(999999999999); + _value->setSuffix("MB"); + break; + default: + break; + } + + adjustSize(); +}