Add 'OBS stats' condition type (#414)

It supports various different types of stats:
* fps
* cpu
* ram
* frame time
* skipped frames
* missed frames
* dropped frames
* data size sent
* bitrate
This commit is contained in:
WarmUpTill 2022-02-26 02:37:54 -08:00 committed by GitHub
parent 88dff37776
commit 4e5aae7e5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 723 additions and 0 deletions

View File

@ -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

View File

@ -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"

View File

@ -0,0 +1,118 @@
#pragma once
#include "macro.hpp"
#include <QWidget>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <util/platform.h>
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<MacroCondition> Create(Macro *m)
{
return std::make_shared<MacroConditionStats>(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<MacroConditionStats> cond = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroCondition> cond)
{
return new MacroConditionStatsEdit(
parent,
std::dynamic_pointer_cast<MacroConditionStats>(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<MacroConditionStats> _entryData;
private:
void SetWidgetVisibility();
bool _loading = true;
};

View File

@ -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<StatsType, std::string> 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<StatsCondition, std::string> 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<int>(_type));
obs_data_set_int(obj, "condition", static_cast<int>(_condition));
return true;
}
bool MacroConditionStats::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_value = obs_data_get_double(obj, "value");
_type = static_cast<StatsType>(obs_data_get_int(obj, "type"));
_condition =
static_cast<StatsCondition>(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<QListView *>(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<MacroConditionStats> 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<std::string, QWidget *> 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<std::mutex> lock(switcher->m);
_entryData->_value = value;
}
void MacroConditionStatsEdit::StatsTypeChanged(int type)
{
if (_loading || !_entryData) {
return;
}
{
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_type = static_cast<StatsType>(type);
SetWidgetVisibility();
}
_value->setValue(0);
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
void MacroConditionStatsEdit::ConditionChanged(int cond)
{
if (_loading || !_entryData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_condition = static_cast<StatsCondition>(cond);
}
void MacroConditionStatsEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_value->setValue(_entryData->_value);
_stats->setCurrentIndex(static_cast<int>(_entryData->_type));
_condition->setCurrentIndex(static_cast<int>(_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();
}