obsLogViewer;
+
+void OBSLogViewer::InitLog()
+{
+ char logDir[512];
+ std::string path;
+
+ if (GetConfigPath(logDir, sizeof(logDir), CONFIG_DIR_NAME "/logs")) {
+ path += logDir;
+ path += "/";
+ path += App()->GetCurrentLog();
+ }
+
+ QFile file(QT_UTF8(path.c_str()));
+
+ if (file.open(QIODevice::ReadOnly)) {
+ QTextStream in(&file);
+ in.setCodec("UTF-8");
+
+ while (!in.atEnd()) {
+ QString line = in.readLine();
+ AddLine(LOG_INFO, line);
+ }
+
+ file.close();
+ }
+
+ obsLogViewer = this;
+}
+
+void OBSLogViewer::AddLine(int type, const QString &str)
+{
+ QString msg = str.toHtmlEscaped();
+
+ switch (type) {
+ case LOG_WARNING:
+ msg = QStringLiteral("") + msg +
+ QStringLiteral("");
+ break;
+ case LOG_ERROR:
+ msg = QStringLiteral("") + msg +
+ QStringLiteral("");
+ break;
+ }
+
+ QScrollBar *scroll = textArea->verticalScrollBar();
+ bool bottomScrolled = scroll->value() >= scroll->maximum() - 10;
+
+ if (bottomScrolled)
+ scroll->setValue(scroll->maximum());
+
+ QTextCursor newCursor = textArea->textCursor();
+ newCursor.movePosition(QTextCursor::End);
+ newCursor.insertHtml(
+ QStringLiteral("") + msg +
+ QStringLiteral("
"));
+
+ if (bottomScrolled)
+ scroll->setValue(scroll->maximum());
+}
+
+void OBSLogViewer::ClearText()
+{
+ textArea->clear();
+}
+
+void OBSLogViewer::OpenFile()
+{
+ char logDir[512];
+ if (GetConfigPath(logDir, sizeof(logDir), CONFIG_DIR_NAME "/logs") <= 0)
+ return;
+
+ const char *log = App()->GetCurrentLog();
+
+ std::string path = logDir;
+ path += "/";
+ path += log;
+
+ QUrl url = QUrl::fromLocalFile(QT_UTF8(path.c_str()));
+ QDesktopServices::openUrl(url);
+}
diff --git a/UI/log-viewer.hpp b/UI/log-viewer.hpp
new file mode 100644
index 0000000..2ca7537
--- /dev/null
+++ b/UI/log-viewer.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include
+#include
+#include "obs-app.hpp"
+
+class OBSLogViewer : public QDialog {
+ Q_OBJECT
+
+ QPointer textArea;
+
+ void InitLog();
+
+private slots:
+ void AddLine(int type, const QString &text);
+ void ClearText();
+ void ToggleShowStartup(bool checked);
+ void OpenFile();
+
+public:
+ OBSLogViewer(QWidget *parent = 0);
+ ~OBSLogViewer();
+};
diff --git a/UI/media-controls.cpp b/UI/media-controls.cpp
new file mode 100644
index 0000000..cc8733e
--- /dev/null
+++ b/UI/media-controls.cpp
@@ -0,0 +1,472 @@
+#include "window-basic-main.hpp"
+#include "media-controls.hpp"
+#include "obs-app.hpp"
+#include
+#include
+#include
+
+#include "ui_media-controls.h"
+
+void MediaControls::OBSMediaStopped(void *data, calldata_t *)
+{
+ MediaControls *media = static_cast(data);
+ QMetaObject::invokeMethod(media, "SetRestartState");
+}
+
+void MediaControls::OBSMediaPlay(void *data, calldata_t *)
+{
+ MediaControls *media = static_cast(data);
+ QMetaObject::invokeMethod(media, "SetPlayingState");
+}
+
+void MediaControls::OBSMediaPause(void *data, calldata_t *)
+{
+ MediaControls *media = static_cast(data);
+ QMetaObject::invokeMethod(media, "SetPausedState");
+}
+
+void MediaControls::OBSMediaStarted(void *data, calldata_t *)
+{
+ MediaControls *media = static_cast(data);
+ QMetaObject::invokeMethod(media, "SetPlayingState");
+}
+
+MediaControls::MediaControls(QWidget *parent)
+ : QWidget(parent), ui(new Ui::MediaControls)
+{
+ ui->setupUi(this);
+ ui->playPauseButton->setProperty("themeID", "playIcon");
+ ui->previousButton->setProperty("themeID", "previousIcon");
+ ui->nextButton->setProperty("themeID", "nextIcon");
+ ui->stopButton->setProperty("themeID", "stopIcon");
+ setFocusPolicy(Qt::StrongFocus);
+
+ connect(&mediaTimer, SIGNAL(timeout()), this,
+ SLOT(SetSliderPosition()));
+ connect(&seekTimer, SIGNAL(timeout()), this, SLOT(SeekTimerCallback()));
+ connect(ui->slider, SIGNAL(sliderPressed()), this,
+ SLOT(MediaSliderClicked()));
+ connect(ui->slider, SIGNAL(mediaSliderHovered(int)), this,
+ SLOT(MediaSliderHovered(int)));
+ connect(ui->slider, SIGNAL(sliderReleased()), this,
+ SLOT(MediaSliderReleased()));
+ connect(ui->slider, SIGNAL(sliderMoved(int)), this,
+ SLOT(MediaSliderMoved(int)));
+
+ countDownTimer = config_get_bool(App()->GlobalConfig(), "BasicWindow",
+ "MediaControlsCountdownTimer");
+
+ QAction *restartAction = new QAction(this);
+ restartAction->setShortcut({Qt::Key_R});
+ connect(restartAction, SIGNAL(triggered()), this, SLOT(RestartMedia()));
+ addAction(restartAction);
+
+ QAction *sliderFoward = new QAction(this);
+ sliderFoward->setShortcutContext(Qt::WidgetWithChildrenShortcut);
+ connect(sliderFoward, SIGNAL(triggered()), this,
+ SLOT(MoveSliderFoward()));
+ sliderFoward->setShortcut({Qt::Key_Right});
+ addAction(sliderFoward);
+
+ QAction *sliderBack = new QAction(this);
+ sliderBack->setShortcutContext(Qt::WidgetWithChildrenShortcut);
+ connect(sliderBack, SIGNAL(triggered()), this,
+ SLOT(MoveSliderBackwards()));
+ sliderBack->setShortcut({Qt::Key_Left});
+ addAction(sliderBack);
+}
+
+MediaControls::~MediaControls()
+{
+ delete ui;
+}
+
+bool MediaControls::MediaPaused()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (!source) {
+ return false;
+ }
+
+ obs_media_state state = obs_source_media_get_state(source);
+ return state == OBS_MEDIA_STATE_PAUSED;
+}
+
+int64_t MediaControls::GetSliderTime(int val)
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (!source) {
+ return 0;
+ }
+
+ float percent = (float)val / (float)ui->slider->maximum();
+ float duration = (float)obs_source_media_get_duration(source);
+ int64_t seekTo = (int64_t)(percent * duration);
+
+ return seekTo;
+}
+
+void MediaControls::MediaSliderClicked()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (!source) {
+ return;
+ }
+
+ obs_media_state state = obs_source_media_get_state(source);
+
+ if (state == OBS_MEDIA_STATE_PAUSED) {
+ prevPaused = true;
+ } else if (state == OBS_MEDIA_STATE_PLAYING) {
+ prevPaused = false;
+ PauseMedia();
+ StopMediaTimer();
+ }
+
+ seek = ui->slider->value();
+ seekTimer.start(100);
+}
+
+void MediaControls::MediaSliderReleased()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (!source) {
+ return;
+ }
+
+ if (seekTimer.isActive()) {
+ seekTimer.stop();
+ if (lastSeek != seek) {
+ obs_source_media_set_time(source, GetSliderTime(seek));
+ }
+
+ seek = lastSeek = -1;
+ }
+
+ if (!prevPaused) {
+ PlayMedia();
+ StartMediaTimer();
+ }
+}
+
+void MediaControls::MediaSliderHovered(int val)
+{
+ float seconds = ((float)GetSliderTime(val) / 1000.0f);
+ QToolTip::showText(QCursor::pos(), FormatSeconds((int)seconds), this);
+}
+
+void MediaControls::MediaSliderMoved(int val)
+{
+ if (seekTimer.isActive()) {
+ seek = val;
+ }
+}
+
+void MediaControls::SeekTimerCallback()
+{
+ if (lastSeek != seek) {
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (source) {
+ obs_source_media_set_time(source, GetSliderTime(seek));
+ }
+ lastSeek = seek;
+ }
+}
+
+void MediaControls::StartMediaTimer()
+{
+ if (isSlideshow)
+ return;
+
+ if (!mediaTimer.isActive())
+ mediaTimer.start(1000);
+}
+
+void MediaControls::StopMediaTimer()
+{
+ if (mediaTimer.isActive())
+ mediaTimer.stop();
+}
+
+void MediaControls::SetPlayingState()
+{
+ ui->slider->setEnabled(true);
+ ui->playPauseButton->setProperty("themeID", "pauseIcon");
+ ui->playPauseButton->style()->unpolish(ui->playPauseButton);
+ ui->playPauseButton->style()->polish(ui->playPauseButton);
+ ui->playPauseButton->setToolTip(
+ QTStr("ContextBar.MediaControls.PauseMedia"));
+
+ prevPaused = false;
+
+ StartMediaTimer();
+}
+
+void MediaControls::SetPausedState()
+{
+ ui->playPauseButton->setProperty("themeID", "playIcon");
+ ui->playPauseButton->style()->unpolish(ui->playPauseButton);
+ ui->playPauseButton->style()->polish(ui->playPauseButton);
+ ui->playPauseButton->setToolTip(
+ QTStr("ContextBar.MediaControls.PlayMedia"));
+
+ StopMediaTimer();
+}
+
+void MediaControls::SetRestartState()
+{
+ ui->playPauseButton->setProperty("themeID", "restartIcon");
+ ui->playPauseButton->style()->unpolish(ui->playPauseButton);
+ ui->playPauseButton->style()->polish(ui->playPauseButton);
+ ui->playPauseButton->setToolTip(
+ QTStr("ContextBar.MediaControls.RestartMedia"));
+
+ ui->slider->setValue(0);
+ ui->timerLabel->setText("--:--:--");
+ ui->durationLabel->setText("--:--:--");
+ ui->slider->setEnabled(false);
+
+ StopMediaTimer();
+}
+
+void MediaControls::RefreshControls()
+{
+ OBSSource source;
+ source = OBSGetStrongRef(weakSource);
+
+ uint32_t flags = 0;
+ const char *id = nullptr;
+
+ if (source) {
+ flags = obs_source_get_output_flags(source);
+ id = obs_source_get_unversioned_id(source);
+ }
+
+ if (!source || !(flags & OBS_SOURCE_CONTROLLABLE_MEDIA)) {
+ SetRestartState();
+ setEnabled(false);
+ hide();
+ return;
+ } else {
+ setEnabled(true);
+ show();
+ }
+
+ bool has_playlist = strcmp(id, "ffmpeg_source") != 0;
+ ui->previousButton->setVisible(has_playlist);
+ ui->nextButton->setVisible(has_playlist);
+
+ isSlideshow = strcmp(id, "slideshow") == 0;
+ ui->slider->setVisible(!isSlideshow);
+ ui->timerLabel->setVisible(!isSlideshow);
+ ui->label->setVisible(!isSlideshow);
+ ui->durationLabel->setVisible(!isSlideshow);
+ ui->emptySpaceAgain->setVisible(isSlideshow);
+
+ obs_media_state state = obs_source_media_get_state(source);
+
+ switch (state) {
+ case OBS_MEDIA_STATE_STOPPED:
+ case OBS_MEDIA_STATE_ENDED:
+ case OBS_MEDIA_STATE_NONE:
+ SetRestartState();
+ break;
+ case OBS_MEDIA_STATE_PLAYING:
+ SetPlayingState();
+ break;
+ case OBS_MEDIA_STATE_PAUSED:
+ SetPausedState();
+ break;
+ default:
+ break;
+ }
+
+ SetSliderPosition();
+}
+
+OBSSource MediaControls::GetSource()
+{
+ return OBSGetStrongRef(weakSource);
+}
+
+void MediaControls::SetSource(OBSSource source)
+{
+ sigs.clear();
+
+ if (source) {
+ weakSource = OBSGetWeakRef(source);
+ signal_handler_t *sh = obs_source_get_signal_handler(source);
+ sigs.emplace_back(sh, "media_play", OBSMediaPlay, this);
+ sigs.emplace_back(sh, "media_pause", OBSMediaPause, this);
+ sigs.emplace_back(sh, "media_restart", OBSMediaPlay, this);
+ sigs.emplace_back(sh, "media_stopped", OBSMediaStopped, this);
+ sigs.emplace_back(sh, "media_started", OBSMediaStarted, this);
+ sigs.emplace_back(sh, "media_ended", OBSMediaStopped, this);
+ } else {
+ weakSource = nullptr;
+ }
+
+ RefreshControls();
+}
+
+void MediaControls::SetSliderPosition()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (!source) {
+ return;
+ }
+
+ float time = (float)obs_source_media_get_time(source);
+ float duration = (float)obs_source_media_get_duration(source);
+
+ float sliderPosition = (time / duration) * (float)ui->slider->maximum();
+
+ ui->slider->setValue((int)sliderPosition);
+
+ ui->timerLabel->setText(FormatSeconds((int)(time / 1000.0f)));
+
+ if (!countDownTimer)
+ ui->durationLabel->setText(
+ FormatSeconds((int)(duration / 1000.0f)));
+ else
+ ui->durationLabel->setText(
+ QString("-") +
+ FormatSeconds((int)((duration - time) / 1000.0f)));
+}
+
+QString MediaControls::FormatSeconds(int totalSeconds)
+{
+ int seconds = totalSeconds % 60;
+ int totalMinutes = totalSeconds / 60;
+ int minutes = totalMinutes % 60;
+ int hours = totalMinutes / 60;
+
+ return QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds);
+}
+
+void MediaControls::on_playPauseButton_clicked()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (!source) {
+ return;
+ }
+
+ obs_media_state state = obs_source_media_get_state(source);
+
+ switch (state) {
+ case OBS_MEDIA_STATE_STOPPED:
+ case OBS_MEDIA_STATE_ENDED:
+ RestartMedia();
+ break;
+ case OBS_MEDIA_STATE_PLAYING:
+ PauseMedia();
+ break;
+ case OBS_MEDIA_STATE_PAUSED:
+ PlayMedia();
+ break;
+ default:
+ break;
+ }
+}
+
+void MediaControls::RestartMedia()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (source) {
+ obs_source_media_restart(source);
+ }
+}
+
+void MediaControls::PlayMedia()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (source) {
+ obs_source_media_play_pause(source, false);
+ }
+}
+
+void MediaControls::PauseMedia()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (source) {
+ obs_source_media_play_pause(source, true);
+ }
+}
+
+void MediaControls::StopMedia()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (source) {
+ obs_source_media_stop(source);
+ }
+}
+
+void MediaControls::PlaylistNext()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (source) {
+ obs_source_media_next(source);
+ }
+}
+
+void MediaControls::PlaylistPrevious()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+ if (source) {
+ obs_source_media_previous(source);
+ }
+}
+
+void MediaControls::on_stopButton_clicked()
+{
+ StopMedia();
+}
+
+void MediaControls::on_nextButton_clicked()
+{
+ PlaylistNext();
+}
+
+void MediaControls::on_previousButton_clicked()
+{
+ PlaylistPrevious();
+}
+
+void MediaControls::on_durationLabel_clicked()
+{
+ countDownTimer = !countDownTimer;
+
+ config_set_bool(App()->GlobalConfig(), "BasicWindow",
+ "MediaControlsCountdownTimer", countDownTimer);
+
+ if (MediaPaused())
+ SetSliderPosition();
+}
+
+void MediaControls::MoveSliderFoward(int seconds)
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+
+ if (!source)
+ return;
+
+ int ms = obs_source_media_get_time(source);
+ ms += seconds * 1000;
+
+ obs_source_media_set_time(source, ms);
+ SetSliderPosition();
+}
+
+void MediaControls::MoveSliderBackwards(int seconds)
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+
+ if (!source)
+ return;
+
+ int ms = obs_source_media_get_time(source);
+ ms -= seconds * 1000;
+
+ obs_source_media_set_time(source, ms);
+ SetSliderPosition();
+}
diff --git a/UI/media-controls.hpp b/UI/media-controls.hpp
new file mode 100644
index 0000000..4dec25c
--- /dev/null
+++ b/UI/media-controls.hpp
@@ -0,0 +1,75 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include "qt-wrappers.hpp"
+
+class Ui_MediaControls;
+
+class MediaControls : public QWidget {
+ Q_OBJECT
+
+private:
+ std::vector sigs;
+ OBSWeakSource weakSource = nullptr;
+ QTimer mediaTimer;
+ QTimer seekTimer;
+ int seek;
+ int lastSeek;
+ bool prevPaused = false;
+ bool countDownTimer = false;
+ bool isSlideshow = false;
+
+ QString FormatSeconds(int totalSeconds);
+ void StartMediaTimer();
+ void StopMediaTimer();
+ void RefreshControls();
+ void SetScene(OBSScene scene);
+ int64_t GetSliderTime(int val);
+
+ static void OBSMediaStopped(void *data, calldata_t *calldata);
+ static void OBSMediaPlay(void *data, calldata_t *calldata);
+ static void OBSMediaPause(void *data, calldata_t *calldata);
+ static void OBSMediaStarted(void *data, calldata_t *calldata);
+
+ Ui_MediaControls *ui;
+
+private slots:
+ void on_playPauseButton_clicked();
+ void on_stopButton_clicked();
+ void on_nextButton_clicked();
+ void on_previousButton_clicked();
+ void on_durationLabel_clicked();
+
+ void MediaSliderClicked();
+ void MediaSliderReleased();
+ void MediaSliderHovered(int val);
+ void MediaSliderMoved(int val);
+ void SetSliderPosition();
+ void SetPlayingState();
+ void SetPausedState();
+ void SetRestartState();
+ void RestartMedia();
+ void StopMedia();
+ void PlaylistNext();
+ void PlaylistPrevious();
+
+ void SeekTimerCallback();
+
+ void MoveSliderFoward(int seconds = 5);
+ void MoveSliderBackwards(int seconds = 5);
+
+public slots:
+ void PlayMedia();
+ void PauseMedia();
+
+public:
+ MediaControls(QWidget *parent = nullptr);
+ ~MediaControls();
+
+ OBSSource GetSource();
+ void SetSource(OBSSource newSource);
+ bool MediaPaused();
+};
diff --git a/UI/media-slider.cpp b/UI/media-slider.cpp
new file mode 100644
index 0000000..c85d762
--- /dev/null
+++ b/UI/media-slider.cpp
@@ -0,0 +1,34 @@
+#include "slider-absoluteset-style.hpp"
+#include "media-slider.hpp"
+#include
+
+MediaSlider::MediaSlider(QWidget *parent) : SliderIgnoreScroll(parent)
+{
+ setMouseTracking(true);
+
+ QString styleName = style()->objectName();
+ QStyle *style;
+ style = QStyleFactory::create(styleName);
+ if (!style) {
+ style = new SliderAbsoluteSetStyle();
+ } else {
+ style = new SliderAbsoluteSetStyle(style);
+ }
+
+ style->setParent(this);
+ this->setStyle(style);
+}
+
+void MediaSlider::mouseMoveEvent(QMouseEvent *event)
+{
+ int val = minimum() + ((maximum() - minimum()) * event->x()) / width();
+
+ if (val > maximum())
+ val = maximum();
+ else if (val < minimum())
+ val = minimum();
+
+ emit mediaSliderHovered(val);
+ event->accept();
+ QSlider::mouseMoveEvent(event);
+}
diff --git a/UI/media-slider.hpp b/UI/media-slider.hpp
new file mode 100644
index 0000000..caed5ab
--- /dev/null
+++ b/UI/media-slider.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include
+#include "slider-ignorewheel.hpp"
+
+class MediaSlider : public SliderIgnoreScroll {
+ Q_OBJECT
+
+public:
+ MediaSlider(QWidget *parent = nullptr);
+
+signals:
+ void mediaSliderHovered(int value);
+
+protected:
+ virtual void mouseMoveEvent(QMouseEvent *event) override;
+};
diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp
index 88c47c0..331c243 100644
--- a/UI/obs-app.cpp
+++ b/UI/obs-app.cpp
@@ -38,6 +38,7 @@
#include "qt-wrappers.hpp"
#include "obs-app.hpp"
+#include "log-viewer.hpp"
#include "window-basic-main.hpp"
#include "window-basic-settings.hpp"
#include "crash-report.hpp"
@@ -49,6 +50,7 @@
#ifdef _WIN32
#include
+#include
#else
#include
#include
@@ -74,18 +76,19 @@ bool opt_start_streaming = false;
bool opt_start_recording = false;
bool opt_studio_mode = false;
bool opt_start_replaybuffer = false;
+bool opt_start_virtualcam = false;
bool opt_minimize_tray = false;
bool opt_allow_opengl = false;
bool opt_always_on_top = false;
+bool opt_disable_updater = false;
string opt_starting_collection;
string opt_starting_profile;
string opt_starting_scene;
-bool remuxAfterRecord = false;
-string remuxFilename;
-
bool restart = false;
+QPointer obsLogViewer;
+
// GPU hint exports for AMD/NVIDIA laptops
#ifdef _MSC_VER
extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1;
@@ -188,6 +191,9 @@ QObject *CreateShortcutFilter()
event->nativeVirtualKey());
}
+ if (event->isAutoRepeat())
+ return true;
+
hotkey.modifiers = TranslateQtKeyboardEventModifiers(
event->modifiers());
@@ -247,12 +253,22 @@ string CurrentDateTimeString()
}
static inline void LogString(fstream &logFile, const char *timeString,
- char *str)
+ char *str, int log_level)
{
- logFile << timeString << str << endl;
+ string msg;
+ msg += timeString;
+ msg += str;
+
+ logFile << msg << endl;
+
+ if (!!obsLogViewer)
+ QMetaObject::invokeMethod(obsLogViewer.data(), "AddLine",
+ Qt::QueuedConnection,
+ Q_ARG(int, log_level),
+ Q_ARG(QString, QString(msg.c_str())));
}
-static inline void LogStringChunk(fstream &logFile, char *str)
+static inline void LogStringChunk(fstream &logFile, char *str, int log_level)
{
char *nextLine = str;
string timeString = CurrentTimeString();
@@ -269,12 +285,12 @@ static inline void LogStringChunk(fstream &logFile, char *str)
nextLine[0] = 0;
}
- LogString(logFile, timeString.c_str(), str);
+ LogString(logFile, timeString.c_str(), str, log_level);
nextLine++;
str = nextLine;
}
- LogString(logFile, timeString.c_str(), str);
+ LogString(logFile, timeString.c_str(), str, log_level);
}
#define MAX_REPEATED_LINES 30
@@ -365,7 +381,7 @@ static void do_log(int log_level, const char *msg, va_list args, void *param)
if (log_level <= LOG_INFO || log_verbose) {
if (too_many_repeated_entries(logFile, msg, str))
return;
- LogStringChunk(logFile, str);
+ LogStringChunk(logFile, str, log_level);
}
#if defined(_WIN32) && defined(OBS_DEBUGBREAK_ON_ERROR)
@@ -467,6 +483,10 @@ bool OBSApp::InitGlobalConfigDefaults()
config_set_default_bool(globalConfig, "Video", "ResetOSXVSyncOnExit",
true);
#endif
+
+ config_set_default_bool(globalConfig, "BasicWindow",
+ "MediaControlsCountdownTimer", true);
+
return true;
}
@@ -1058,7 +1078,7 @@ bool OBSApp::SetTheme(std::string name, std::string path)
if (path == "") {
char userDir[512];
name = "themes/" + name + ".qss";
- string temp = "obs-studio/" + name;
+ string temp = CONFIG_DIR_NAME "/" + name;
int ret = GetConfigPath(userDir, sizeof(userDir), temp.c_str());
if (ret > 0 && QFile::exists(userDir)) {
@@ -1134,6 +1154,9 @@ OBSApp::~OBSApp()
os_inhibit_sleep_set_active(sleepInhibitor, false);
os_inhibit_sleep_destroy(sleepInhibitor);
+
+ if (libobs_initialized)
+ obs_shutdown();
}
static void move_basic_to_profiles(void)
@@ -1355,6 +1378,8 @@ bool OBSApp::OBSInit()
if (!StartupOBS(locale.c_str(), GetProfilerNameStore()))
return false;
+ libobs_initialized = true;
+
obs_set_ui_task_handler(ui_task_handler);
#ifdef _WIN32
@@ -1428,6 +1453,11 @@ bool OBSApp::IsPortableMode()
return portable_mode;
}
+bool OBSApp::IsUpdaterDisabled()
+{
+ return opt_disable_updater;
+}
+
#ifdef __APPLE__
#define INPUT_AUDIO_SOURCE "coreaudio_input_capture"
#define OUTPUT_AUDIO_SOURCE "coreaudio_output_capture"
@@ -1634,20 +1664,123 @@ string GenerateTimeDateFilename(const char *extension, bool noSpace)
string GenerateSpecifiedFilename(const char *extension, bool noSpace,
const char *format)
+{
+ BPtr filename =
+ os_generate_formatted_filename(extension, !noSpace, format);
+ return string(filename);
+}
+
+static void FindBestFilename(string &strPath, bool noSpace)
+{
+ int num = 2;
+
+ if (!os_file_exists(strPath.c_str()))
+ return;
+
+ const char *ext = strrchr(strPath.c_str(), '.');
+ if (!ext)
+ return;
+
+ int extStart = int(ext - strPath.c_str());
+ for (;;) {
+ string testPath = strPath;
+ string numStr;
+
+ numStr = noSpace ? "_" : " (";
+ numStr += to_string(num++);
+ if (!noSpace)
+ numStr += ")";
+
+ testPath.insert(extStart, numStr);
+
+ if (!os_file_exists(testPath.c_str())) {
+ strPath = testPath;
+ break;
+ }
+ }
+}
+
+static void ensure_directory_exists(string &path)
+{
+ replace(path.begin(), path.end(), '\\', '/');
+
+ size_t last = path.rfind('/');
+ if (last == string::npos)
+ return;
+
+ string directory = path.substr(0, last);
+ os_mkdirs(directory.c_str());
+}
+
+static void remove_reserved_file_characters(string &s)
+{
+ replace(s.begin(), s.end(), '\\', '/');
+ replace(s.begin(), s.end(), '*', '_');
+ replace(s.begin(), s.end(), '?', '_');
+ replace(s.begin(), s.end(), '"', '_');
+ replace(s.begin(), s.end(), '|', '_');
+ replace(s.begin(), s.end(), ':', '_');
+ replace(s.begin(), s.end(), '>', '_');
+ replace(s.begin(), s.end(), '<', '_');
+}
+
+string GetFormatString(const char *format, const char *prefix,
+ const char *suffix)
+{
+ string f;
+
+ if (prefix && *prefix) {
+ f += prefix;
+ if (f.back() != ' ')
+ f += " ";
+ }
+
+ f += format;
+
+ if (suffix && *suffix) {
+ if (*suffix != ' ')
+ f += " ";
+ f += suffix;
+ }
+
+ remove_reserved_file_characters(f);
+
+ return f;
+}
+
+string GetOutputFilename(const char *path, const char *ext, bool noSpace,
+ bool overwrite, const char *format)
{
OBSBasic *main = reinterpret_cast(App()->GetMainWindow());
- bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux");
- if ((strcmp(extension, "mp4") == 0) && autoRemux)
- extension = "mkv";
+ os_dir_t *dir = path && path[0] ? os_opendir(path) : nullptr;
- BPtr filename =
- os_generate_formatted_filename(extension, !noSpace, format);
+ if (!dir) {
+ if (main->isVisible())
+ OBSMessageBox::warning(main,
+ QTStr("Output.BadPath.Title"),
+ QTStr("Output.BadPath.Text"));
+ else
+ main->SysTrayNotify(QTStr("Output.BadPath.Text"),
+ QSystemTrayIcon::Warning);
+ return "";
+ }
- remuxFilename = string(filename);
- remuxAfterRecord = autoRemux;
+ os_closedir(dir);
- return string(filename);
+ string strPath;
+ strPath += path;
+
+ char lastChar = strPath.back();
+ if (lastChar != '/' && lastChar != '\\')
+ strPath += "/";
+
+ strPath += GenerateSpecifiedFilename(ext, noSpace, format);
+ ensure_directory_exists(strPath);
+ if (!overwrite)
+ FindBestFilename(strPath, noSpace);
+
+ return strPath;
}
vector> GetLocaleNames()
@@ -1854,6 +1987,22 @@ static int run_program(fstream &logFile, int argc, char *argv[])
run:
#endif
+#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__FreeBSD__)
+ // Mounted by termina during chromeOS linux container startup
+ // https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/master/project-termina/chromeos-base/termina-lxd-scripts/files/lxd_setup.sh
+ os_dir_t *crosDir = os_opendir("/opt/google/cros-containers");
+ if (crosDir) {
+ QMessageBox::StandardButtons buttons(QMessageBox::Ok);
+ QMessageBox mb(QMessageBox::Critical,
+ QTStr("ChromeOS.Title"),
+ QTStr("ChromeOS.Text"), buttons,
+ nullptr);
+
+ mb.exec();
+ return 0;
+ }
+#endif
+
if (!created_log) {
create_log_file(logFile);
created_log = true;
@@ -1894,8 +2043,7 @@ static int run_program(fstream &logFile, int argc, char *argv[])
#define CRASH_MESSAGE \
"Woops, OBS has crashed!\n\nWould you like to copy the crash log " \
- "to the clipboard? (Crash logs will still be saved to the " \
- "%appdata%\\" CONFIG_DIR_NAME "\\crashes directory)"
+ "to the clipboard? The crash log will still be saved to:\n\n%s"
static void main_crash_handler(const char *format, va_list args, void *param)
{
@@ -1904,10 +2052,12 @@ static void main_crash_handler(const char *format, va_list args, void *param)
vsnprintf(text, MAX_CRASH_REPORT_SIZE, format, args);
text[MAX_CRASH_REPORT_SIZE - 1] = 0;
- delete_oldest_file(true, CONFIG_DIR_NAME "/crashes");
+ string crashFilePath = CONFIG_DIR_NAME "/crashes";
+
+ delete_oldest_file(true, crashFilePath.c_str());
- string name = CONFIG_DIR_NAME "/crashes/Crash ";
- name += GenerateTimeDateFilename("txt");
+ string name = crashFilePath + "/";
+ name += "Crash " + GenerateTimeDateFilename("txt");
BPtr path(GetConfigPathPtr(name.c_str()));
@@ -1925,7 +2075,26 @@ static void main_crash_handler(const char *format, va_list args, void *param)
file << text;
file.close();
- int ret = MessageBoxA(NULL, CRASH_MESSAGE, "OBS has crashed!",
+ string pathString(path.Get());
+
+#ifdef _WIN32
+ std::replace(pathString.begin(), pathString.end(), '/', '\\');
+#endif
+
+ string absolutePath =
+ canonical(filesystem::path(pathString)).u8string();
+
+ size_t size = snprintf(nullptr, 0, CRASH_MESSAGE, absolutePath.c_str());
+
+ unique_ptr message_buffer(new char[size + 1]);
+
+ snprintf(message_buffer.get(), size + 1, CRASH_MESSAGE,
+ absolutePath.c_str());
+
+ string finalMessage =
+ string(message_buffer.get(), message_buffer.get() + size);
+
+ int ret = MessageBoxA(NULL, finalMessage.c_str(), "OBS has crashed!",
MB_YESNO | MB_ICONERROR | MB_TASKMODAL);
if (ret == IDYES) {
@@ -2410,6 +2579,9 @@ int main(int argc, char *argv[])
} else if (arg_is(argv[i], "--startreplaybuffer", nullptr)) {
opt_start_replaybuffer = true;
+ } else if (arg_is(argv[i], "--startvirtualcam", nullptr)) {
+ opt_start_virtualcam = true;
+
} else if (arg_is(argv[i], "--collection", nullptr)) {
if (++i < argc)
opt_starting_collection = argv[i];
@@ -2431,26 +2603,36 @@ int main(int argc, char *argv[])
} else if (arg_is(argv[i], "--allow-opengl", nullptr)) {
opt_allow_opengl = true;
+ } else if (arg_is(argv[i], "--disable-updater", nullptr)) {
+ opt_disable_updater = true;
+
} else if (arg_is(argv[i], "--help", "-h")) {
- std::cout
- << "--help, -h: Get list of available commands.\n\n"
- << "--startstreaming: Automatically start streaming.\n"
- << "--startrecording: Automatically start recording.\n"
- << "--startreplaybuffer: Start replay buffer.\n\n"
- << "--collection : Use specific scene collection."
- << "\n"
- << "--profile : Use specific profile.\n"
- << "--scene : Start with specific scene.\n\n"
- << "--studio-mode: Enable studio mode.\n"
- << "--minimize-to-tray: Minimize to system tray.\n"
- << "--portable, -p: Use portable mode.\n"
- << "--multi, -m: Don't warn when launching multiple instances.\n\n"
- << "--verbose: Make log more verbose.\n"
- << "--always-on-top: Start in 'always on top' mode.\n\n"
- << "--unfiltered_log: Make log unfiltered.\n\n"
- << "--allow-opengl: Allow OpenGL on Windows.\n\n"
- << "--version, -V: Get current version.\n";
+ std::string help =
+ "--help, -h: Get list of available commands.\n\n"
+ "--startstreaming: Automatically start streaming.\n"
+ "--startrecording: Automatically start recording.\n"
+ "--startreplaybuffer: Start replay buffer.\n"
+ "--startvirtualcam: Start virtual camera (if available).\n\n"
+ "--collection : Use specific scene collection."
+ "\n"
+ "--profile : Use specific profile.\n"
+ "--scene : Start with specific scene.\n\n"
+ "--studio-mode: Enable studio mode.\n"
+ "--minimize-to-tray: Minimize to system tray.\n"
+ "--portable, -p: Use portable mode.\n"
+ "--multi, -m: Don't warn when launching multiple instances.\n\n"
+ "--verbose: Make log more verbose.\n"
+ "--always-on-top: Start in 'always on top' mode.\n\n"
+ "--unfiltered_log: Make log unfiltered.\n\n"
+ "--disable-updater: Disable built-in updater (Windows/Mac only)\n\n";
+#ifdef _WIN32
+ MessageBoxA(NULL, help.c_str(), "Help",
+ MB_OK | MB_ICONASTERISK);
+#else
+ std::cout << help
+ << "--version, -V: Get current version.\n";
+#endif
exit(0);
} else if (arg_is(argv[i], "--version", "-V")) {
@@ -2468,6 +2650,12 @@ int main(int argc, char *argv[])
os_file_exists(BASE_PATH "/portable_mode.txt") ||
os_file_exists(BASE_PATH "/obs_portable_mode.txt");
}
+
+ if (!opt_disable_updater) {
+ opt_disable_updater =
+ os_file_exists(BASE_PATH "/disable_updater") ||
+ os_file_exists(BASE_PATH "/disable_updater.txt");
+ }
#endif
upgrade_settings();
diff --git a/UI/obs-app.hpp b/UI/obs-app.hpp
index de5cac7..3ffd4bd 100644
--- a/UI/obs-app.hpp
+++ b/UI/obs-app.hpp
@@ -40,6 +40,10 @@ std::string GenerateTimeDateFilename(const char *extension,
bool noSpace = false);
std::string GenerateSpecifiedFilename(const char *extension, bool noSpace,
const char *format);
+std::string GetFormatString(const char *format, const char *prefix,
+ const char *suffix);
+std::string GetOutputFilename(const char *path, const char *ext, bool noSpace,
+ bool overwrite, const char *format);
QObject *CreateShortcutFilter();
struct BaseLexer {
@@ -72,10 +76,11 @@ class OBSApp : public QApplication {
std::string theme;
ConfigFile globalConfig;
TextLookup textLookup;
- OBSContext obsContext;
QPointer mainWindow;
profiler_name_store_t *profilerNameStore = nullptr;
+ bool libobs_initialized = false;
+
os_inhibit_t *sleepInhibitor = nullptr;
int sleepInhibitRefs = 0;
@@ -144,6 +149,7 @@ class OBSApp : public QApplication {
std::string GetVersionString() const;
bool IsPortableMode();
+ bool IsUpdaterDisabled();
const char *InputAudioSource() const;
const char *OutputAudioSource() const;
@@ -219,12 +225,10 @@ static inline int GetProfilePath(char *path, size_t size, const char *file)
extern bool portable_mode;
-extern bool remuxAfterRecord;
-extern std::string remuxFilename;
-
extern bool opt_start_streaming;
extern bool opt_start_recording;
extern bool opt_start_replaybuffer;
+extern bool opt_start_virtualcam;
extern bool opt_minimize_tray;
extern bool opt_studio_mode;
extern bool opt_allow_opengl;
diff --git a/UI/obs-frontend-api/CMakeLists.txt b/UI/obs-frontend-api/CMakeLists.txt
index 445cfef..1df1231 100644
--- a/UI/obs-frontend-api/CMakeLists.txt
+++ b/UI/obs-frontend-api/CMakeLists.txt
@@ -13,23 +13,28 @@ if(WIN32)
list(APPEND obs-frontend-api_SOURCES
obs-frontend-api.rc)
endif()
+
+set(obs-frontend-api_PUBLIC_HEADERS
+ obs-frontend-api.h)
+
set(obs-frontend-api_HEADERS
obs-frontend-internal.hpp
- obs-frontend-api.h)
+ ${obs-frontend-api_PUBLIC_HEADERS})
add_library(obs-frontend-api SHARED
${obs-frontend-api_SOURCES}
${obs-frontend-api_HEADERS})
target_link_libraries(obs-frontend-api
libobs)
+set_target_properties(obs-frontend-api PROPERTIES FOLDER "frontend")
if(UNIX AND NOT APPLE)
set_target_properties(obs-frontend-api
PROPERTIES
OUTPUT_NAME obs-frontend-api
- VERSION 0.0
- SOVERSION 0
+ VERSION 0
)
endif()
install_obs_core(obs-frontend-api)
+install_obs_headers(${obs-frontend-api_PUBLIC_HEADERS})
diff --git a/UI/obs-frontend-api/obs-frontend-api.cpp b/UI/obs-frontend-api/obs-frontend-api.cpp
index 8066666..88c0ee2 100644
--- a/UI/obs-frontend-api/obs-frontend-api.cpp
+++ b/UI/obs-frontend-api/obs-frontend-api.cpp
@@ -142,6 +142,18 @@ void obs_frontend_set_transition_duration(int duration)
c->obs_frontend_set_transition_duration(duration);
}
+void obs_frontend_release_tbar(void)
+{
+ if (callbacks_valid())
+ c->obs_frontend_release_tbar();
+}
+
+void obs_frontend_set_tbar_position(int position)
+{
+ if (callbacks_valid())
+ c->obs_frontend_set_tbar_position(position);
+}
+
char **obs_frontend_get_scene_collections(void)
{
if (!callbacks_valid())
@@ -451,3 +463,15 @@ void obs_frontend_set_current_preview_scene(obs_source_t *scene)
if (callbacks_valid())
c->obs_frontend_set_current_preview_scene(scene);
}
+
+void obs_frontend_take_screenshot(void)
+{
+ if (callbacks_valid())
+ c->obs_frontend_take_screenshot();
+}
+
+void obs_frontend_take_source_screenshot(obs_source_t *source)
+{
+ if (callbacks_valid())
+ c->obs_frontend_take_source_screenshot(source);
+}
diff --git a/UI/obs-frontend-api/obs-frontend-api.h b/UI/obs-frontend-api/obs-frontend-api.h
index 18e9d4e..83dd9c7 100644
--- a/UI/obs-frontend-api/obs-frontend-api.h
+++ b/UI/obs-frontend-api/obs-frontend-api.h
@@ -100,6 +100,8 @@ EXPORT obs_source_t *obs_frontend_get_current_transition(void);
EXPORT void obs_frontend_set_current_transition(obs_source_t *transition);
EXPORT int obs_frontend_get_transition_duration(void);
EXPORT void obs_frontend_set_transition_duration(int duration);
+EXPORT void obs_frontend_release_tbar(void);
+EXPORT void obs_frontend_set_tbar_position(int position);
EXPORT char **obs_frontend_get_scene_collections(void);
EXPORT char *obs_frontend_get_current_scene_collection(void);
@@ -192,6 +194,9 @@ EXPORT bool obs_frontend_preview_enabled(void);
EXPORT obs_source_t *obs_frontend_get_current_preview_scene(void);
EXPORT void obs_frontend_set_current_preview_scene(obs_source_t *scene);
+EXPORT void obs_frontend_take_screenshot(void);
+EXPORT void obs_frontend_take_source_screenshot(obs_source_t *source);
+
/* ------------------------------------------------------------------------- */
#ifdef __cplusplus
diff --git a/UI/obs-frontend-api/obs-frontend-internal.hpp b/UI/obs-frontend-api/obs-frontend-internal.hpp
index a5ba063..64ecf9b 100644
--- a/UI/obs-frontend-api/obs-frontend-internal.hpp
+++ b/UI/obs-frontend-api/obs-frontend-internal.hpp
@@ -23,6 +23,8 @@ struct obs_frontend_callbacks {
obs_frontend_set_current_transition(obs_source_t *transition) = 0;
virtual int obs_frontend_get_transition_duration(void) = 0;
virtual void obs_frontend_set_transition_duration(int duration) = 0;
+ virtual void obs_frontend_release_tbar(void) = 0;
+ virtual void obs_frontend_set_tbar_position(int position) = 0;
virtual void obs_frontend_get_scene_collections(
std::vector &strings) = 0;
@@ -116,6 +118,10 @@ struct obs_frontend_callbacks {
virtual void on_preload(obs_data_t *settings) = 0;
virtual void on_save(obs_data_t *settings) = 0;
virtual void on_event(enum obs_frontend_event event) = 0;
+
+ virtual void obs_frontend_take_screenshot() = 0;
+ virtual void
+ obs_frontend_take_source_screenshot(obs_source_t *source) = 0;
};
EXPORT void
diff --git a/UI/obs-proxy-style.cpp b/UI/obs-proxy-style.cpp
new file mode 100644
index 0000000..72b1248
--- /dev/null
+++ b/UI/obs-proxy-style.cpp
@@ -0,0 +1,77 @@
+#include "obs-proxy-style.hpp"
+#include
+
+static inline uint qt_intensity(uint r, uint g, uint b)
+{
+ /* 30% red, 59% green, 11% blue */
+ return (77 * r + 150 * g + 28 * b) / 255;
+}
+
+/* The constants in the default QT styles don't dim the icons enough in
+ * disabled mode
+ *
+ * https://code.woboq.org/qt5/qtbase/src/widgets/styles/qcommonstyle.cpp.html#6429
+ */
+QPixmap OBSProxyStyle::generatedIconPixmap(QIcon::Mode iconMode,
+ const QPixmap &pixmap,
+ const QStyleOption *option) const
+{
+ if (iconMode == QIcon::Disabled) {
+ QImage im =
+ pixmap.toImage().convertToFormat(QImage::Format_ARGB32);
+
+ /* Create a colortable based on the background
+ * (black -> bg -> white) */
+
+ QColor bg = option->palette.color(QPalette::Disabled,
+ QPalette::Window);
+ int red = bg.red();
+ int green = bg.green();
+ int blue = bg.blue();
+ uchar reds[256], greens[256], blues[256];
+
+ for (int i = 0; i < 128; ++i) {
+ reds[i] = uchar((red * (i << 1)) >> 8);
+ greens[i] = uchar((green * (i << 1)) >> 8);
+ blues[i] = uchar((blue * (i << 1)) >> 8);
+ }
+ for (int i = 0; i < 128; ++i) {
+ reds[i + 128] = uchar(qMin(red + (i << 1), 255));
+ greens[i + 128] = uchar(qMin(green + (i << 1), 255));
+ blues[i + 128] = uchar(qMin(blue + (i << 1), 255));
+ }
+
+ /* High intensity colors needs dark shifting in the color
+ * table, while low intensity colors needs light shifting. This
+ * is to increase the perceived contrast. */
+
+ int intensity = qt_intensity(red, green, blue);
+ const int factor = 191;
+
+ if ((red - factor > green && red - factor > blue) ||
+ (green - factor > red && green - factor > blue) ||
+ (blue - factor > red && blue - factor > green))
+ qMin(255, intensity + 20);
+ else if (intensity <= 128)
+ intensity += 100;
+
+ for (int y = 0; y < im.height(); ++y) {
+ QRgb *scanLine = (QRgb *)im.scanLine(y);
+ for (int x = 0; x < im.width(); ++x) {
+ QRgb pixel = *scanLine;
+ /* Calculate color table index, taking
+ * intensity adjustment and a magic offset into
+ * account. */
+ uint ci = uint(qGray(pixel) / 3 +
+ (130 - intensity / 3));
+ *scanLine = qRgba(reds[ci], greens[ci],
+ blues[ci], qAlpha(pixel));
+ ++scanLine;
+ }
+ }
+
+ return QPixmap::fromImage(im);
+ }
+
+ return QProxyStyle::generatedIconPixmap(iconMode, pixmap, option);
+}
diff --git a/UI/obs-proxy-style.hpp b/UI/obs-proxy-style.hpp
new file mode 100644
index 0000000..29ddd57
--- /dev/null
+++ b/UI/obs-proxy-style.hpp
@@ -0,0 +1,9 @@
+#pragma once
+
+#include
+
+class OBSProxyStyle : public QProxyStyle {
+public:
+ QPixmap generatedIconPixmap(QIcon::Mode iconMode, const QPixmap &pixmap,
+ const QStyleOption *option) const override;
+};
diff --git a/UI/panel-reddit-ama.cpp b/UI/panel-reddit-ama.cpp
new file mode 100644
index 0000000..d2c9a1f
--- /dev/null
+++ b/UI/panel-reddit-ama.cpp
@@ -0,0 +1,458 @@
+#include
+
+#include "panel-reddit-ama.hpp"
+
+
+#include "auth-reddit.hpp"
+#include "obs-app.hpp"
+#include "platform.hpp"
+#include "window-basic-main.hpp"
+
+namespace {
+void on_frontend_event(enum obs_frontend_event event, void *param);
+}
+
+AMAModel *RedditAMAPanel::model;
+
+#define PAGE_OFFLINE 0
+#define PAGE_STREAMING 1
+
+RedditAMAPanel::RedditAMAPanel(QWidget *parent)
+ : QDockWidget(parent),
+ ui(new Ui::RpanAMAPanel)
+{
+ ui->setupUi(this);
+
+ ui->usernameLabel->setText("");
+ ui->questionLabel->setText("");
+ ui->dismissButton->setEnabled(false);
+
+ model = new AMAModel(ui->incomingQuestionTable);
+ model->addQuestion("foo", "bar");
+
+ ui->incomingQuestionTable->setModel(model);
+ ui->incomingQuestionTable->horizontalHeader()->setSectionResizeMode(
+ 0, QHeaderView::Stretch);
+ ui->incomingQuestionTable->horizontalHeader()->setSectionResizeMode(
+ 1, QHeaderView::Fixed);
+ ui->incomingQuestionTable->horizontalHeader()->setSectionResizeMode(
+ 2, QHeaderView::Fixed);
+
+ ui->incomingQuestionTable->setColumnWidth(1, 32);
+ ui->incomingQuestionTable->setColumnWidth(2, 32);
+
+ connect(ui->incomingQuestionTable->selectionModel(),
+ &QItemSelectionModel::currentChanged,
+ this,
+ &RedditAMAPanel::OnSelectionChanged);
+
+ connect(ui->dismissButton,
+ &QPushButton::clicked,
+ this,
+ &RedditAMAPanel::OnDismissQuestion);
+
+ DestroyOverlay();
+ SetPage(PAGE_STREAMING);
+
+ obs_frontend_add_event_callback(on_frontend_event, this);
+}
+
+RedditAMAPanel::~RedditAMAPanel()
+{
+ delete model;
+ model = nullptr;
+}
+
+bool RedditAMAPanel::IsActive()
+{
+ auto config = OBSBasic::Get()->Config();
+ return config_get_bool(config, "Reddit", "AMAMode");
+}
+
+void RedditAMAPanel::SetPage(int page)
+{
+ switch (page) {
+ case PAGE_STREAMING:
+ ClearPanel();
+ model->clear();
+ break;
+ case PAGE_OFFLINE:
+ HideOverlay();
+ break;
+ }
+ ui->stackedWidget->setCurrentIndex(page);
+}
+
+void RedditAMAPanel::HideOverlay()
+{
+ OBSScene scene =
+ obs_scene_from_source(obs_frontend_get_current_scene());
+ if (!scene) {
+ return;
+ }
+
+ OBSSceneItem overlay = obs_scene_find_source(
+ scene, "#AMA_GROUP");
+
+ if (overlay) {
+ obs_sceneitem_set_visible(overlay, false);
+ }
+}
+
+void RedditAMAPanel::DestroyOverlay()
+{
+ OBSScene scene = obs_scene_from_source(obs_frontend_get_current_scene());
+ if (!scene) {
+ return;
+ }
+
+ OBSSceneItem overlay =
+ obs_scene_find_source(scene, "#AMA_GROUP");
+
+ if (overlay) {
+ obs_sceneitem_remove(overlay);
+ ClearPanel();
+ }
+}
+
+void RedditAMAPanel::RemoveUI()
+{
+ auto main = OBSBasic::Get();
+ auto auth = static_cast(main->GetAuth());
+ auto panel = auth->amaPanel.get();
+ auto menu = auth->amaMenu.get();
+ panel->close();
+ menu->setVisible(false);
+}
+
+
+void RedditAMAPanel::ClearPanel()
+{
+ ui->usernameLabel->setText("");
+ ui->questionLabel->setText("");
+ ui->dismissButton->setEnabled(false);
+}
+
+OBSSceneItem RedditAMAPanel::CreateOverlay()
+{
+ OBSScene scene =
+ obs_scene_from_source(obs_frontend_get_current_scene());
+ OBSSceneItem group =
+ obs_scene_add_group2(scene, "#AMA_GROUP", true);
+
+ OBSData settings = obs_data_create();
+ obs_data_set_bool(settings, "hidden", true);
+ obs_source_update(obs_sceneitem_get_source(group), settings);
+
+ obs_sceneitem_set_visible(group, false);
+ obs_sceneitem_set_locked(group, true);
+ obs_sceneitem_set_alignment(group, OBS_ALIGN_BOTTOM | OBS_ALIGN_LEFT);
+
+ CreateOverlayBackground(scene, group);
+ CreateOverlayLogo(scene, group);
+ CreateOverlayUsername(scene, group);
+ CreateOverlayQuestion(scene, group);
+
+ vec2 pos;
+ pos.x = 0; // TODO: scene width / 2
+ pos.y = 854; // TODO: scene height
+ obs_sceneitem_set_pos(group, &pos);
+
+ return group;
+}
+
+void RedditAMAPanel::CreateOverlayBackground(OBSScene scene, OBSSceneItem group)
+{
+ OBSData settings = obs_data_create();
+ obs_data_set_int(settings, "color", 0xFF4accff);
+ obs_data_set_int(settings, "width", 480);
+ obs_data_set_int(settings, "height", 192);
+ obs_data_set_bool(settings, "hidden", true);
+ OBSSource bg = obs_source_create("color_source", "#AMA_BG",
+ settings, nullptr);
+
+ OBSData opacitySettings = obs_data_create();
+ obs_data_set_int(opacitySettings, "opacity", 95);
+ OBSSource colorFilter = obs_source_create(
+ "color_filter", "Opacity", opacitySettings, nullptr);
+ obs_source_filter_add(bg, colorFilter);
+
+ OBSSceneItem bgItem = obs_scene_add(scene, bg);
+ obs_sceneitem_group_add_item(group, bgItem);
+}
+
+void RedditAMAPanel::CreateOverlayUsername(OBSScene scene, OBSSceneItem group)
+{
+ OBSData settings = obs_data_create();
+ obs_data_set_int(settings, "custom_width", 386);
+ obs_data_set_bool(settings, "word_wrap", true);
+ obs_data_set_int(settings, "color1", 0xff000000);
+ obs_data_set_int(settings, "color2", 0xFF000000);
+ obs_data_set_bool(settings, "hidden", true);
+ OBSData fontSettings = obs_data_create();
+ obs_data_set_string(fontSettings, "face", "Arial");
+ obs_data_set_int(fontSettings, "size", 24);
+ obs_data_set_string(fontSettings, "style", "Regular");
+ obs_data_set_int(fontSettings, "flags", 0);
+ obs_data_set_obj(settings, "font", fontSettings);
+ OBSSource text = obs_source_create("text_ft2_source",
+ "#AMA_USERNAME", settings,
+ nullptr);
+
+ OBSSceneItem textItem = obs_scene_add(scene, text);
+ obs_sceneitem_group_add_item(group, textItem);
+
+ vec2 pos;
+ pos.x = 88;
+ pos.y = 24;
+ obs_sceneitem_set_pos(textItem, &pos);
+ obs_sceneitem_set_order(textItem, OBS_ORDER_MOVE_TOP);
+}
+
+void RedditAMAPanel::CreateOverlayQuestion(OBSScene scene, OBSSceneItem group)
+{
+ OBSData settings = obs_data_create();
+ obs_data_set_int(settings, "custom_width", 386);
+ obs_data_set_bool(settings, "word_wrap", true);
+ obs_data_set_int(settings, "color1", 0xFF000000);
+ obs_data_set_int(settings, "color2", 0xFF000000);
+ obs_data_set_bool(settings, "hidden", true);
+ OBSData fontSettings = obs_data_create();
+ obs_data_set_string(fontSettings, "face", "Arial");
+ obs_data_set_int(fontSettings, "size", 26);
+ obs_data_set_string(fontSettings, "style", "Regular");
+ obs_data_set_int(fontSettings, "flags", 0);
+ obs_data_set_obj(settings, "font", fontSettings);
+ OBSSource text = obs_source_create("text_ft2_source",
+ "#AMA_QUESTION", settings,
+ nullptr);
+
+ OBSSceneItem textItem = obs_scene_add(scene, text);
+ obs_sceneitem_group_add_item(group, textItem);
+
+ vec2 pos;
+ pos.x = 88;
+ pos.y = 58;
+ obs_sceneitem_set_pos(textItem, &pos);
+ obs_sceneitem_set_order(textItem, OBS_ORDER_MOVE_TOP);
+}
+
+void RedditAMAPanel::CreateOverlayLogo(OBSScene scene, OBSSceneItem group)
+{
+ std::string logoPath;
+ GetDataFilePath("images/reddit.png", logoPath);
+
+ OBSData settings = obs_data_create();
+ obs_data_set_string(settings, "file", logoPath.c_str());
+ obs_data_set_bool(settings, "hidden", true);
+ OBSSource image = obs_source_create("image_source", "#AMA_REDDIT",
+ settings, nullptr);
+
+ OBSSceneItem imageItem = obs_scene_add(scene, image);
+ obs_sceneitem_group_add_item(group, imageItem);
+
+ vec2 pos;
+ pos.x = 16.0;
+ pos.y = 16.0;
+ obs_sceneitem_set_pos(imageItem, &pos);
+ obs_sceneitem_set_order(imageItem, OBS_ORDER_MOVE_TOP);
+}
+
+void RedditAMAPanel::OnSelectionChanged(const QModelIndex ¤t,
+ const QModelIndex &)
+{
+ QSharedPointer question = model->getQuestion(current.row());
+ if (question.isNull()) {
+ return;
+ }
+
+ OBSScene scene = obs_scene_from_source(
+ obs_frontend_get_current_scene());
+ OBSSceneItem overlay = obs_scene_find_source(scene, "#AMA_GROUP");
+
+ switch (current.column()) {
+ case 1: {
+ ui->usernameLabel->setText(question->username);
+ ui->questionLabel->setText(question->question);
+ ui->dismissButton->setEnabled(true);
+ ui->incomingQuestionTable->selectColumn(0);
+ model->removeQuestion(current.row());
+
+ // TODO: save timecode for TOC
+
+ if (!overlay) {
+ overlay = CreateOverlay();
+ }
+ obs_sceneitem_set_order(overlay, OBS_ORDER_MOVE_TOP);
+ obs_sceneitem_set_visible(overlay, true);
+
+ OBSScene overlayGroup =
+ obs_sceneitem_group_get_scene(overlay);
+
+ OBSSceneItem usernameItem = obs_scene_find_source(
+ overlayGroup, "#AMA_USERNAME");
+ if (usernameItem) {
+ OBSSource username = obs_sceneitem_get_source(
+ usernameItem);
+ OBSData settings =
+ obs_source_get_settings(username);
+ obs_data_set_string(settings, "text",
+ QString("u/%1")
+ .arg(question->username)
+ .toUtf8());
+ obs_source_update(username, settings);
+ }
+ OBSSceneItem questionItem = obs_scene_find_source(
+ overlayGroup, "#AMA_QUESTION");
+ if (questionItem) {
+ OBSSource q = obs_sceneitem_get_source(
+ questionItem);
+ OBSData settings =
+ obs_source_get_settings(q);
+ obs_data_set_string(settings, "text",
+ question->question.toUtf8());
+ obs_source_update(q, settings);
+ obs_source_update_properties(q);
+ }
+
+ break;
+ }
+ case 2:
+ ui->incomingQuestionTable->selectColumn(0);
+ model->removeQuestion(current.row());
+ if (overlay) {
+ obs_sceneitem_set_visible(overlay, false);
+ }
+ break;
+ }
+}
+
+void RedditAMAPanel::OnDismissQuestion()
+{
+ ClearPanel();
+ // save timestamp and question
+ HideOverlay();
+}
+
+AMAModel::AMAModel(QWidget *parent)
+ : QAbstractTableModel(parent)
+{
+ acceptIcon.reset(new QPixmap(":/res/images/ok_32.png"));
+ removeIcon.reset(new QPixmap(":/res/images/invalid_32.png"));
+}
+
+QVariant AMAModel::data(const QModelIndex &index, int role) const
+{
+ if (index.isValid() &&
+ index.row() < questions.size() &&
+ index.row() >= 0) {
+ switch (role) {
+ case Qt::DisplayRole:
+ if (index.column() == 0) {
+ auto q = questions[index.row()];
+ auto cell = QString("%1\n%2")
+ .arg(q->username)
+ .arg(q->question);
+ return cell;
+ }
+ break;
+ case Qt::DecorationRole:
+ switch (index.column()) {
+ case 1:
+ return *acceptIcon;
+ case 2:
+ return *removeIcon;
+ }
+ case Qt::SizeHintRole:
+ if (index.column() != 0) {
+ return acceptIcon->size();
+ }
+ break;
+ }
+ }
+ return QVariant();
+}
+
+QVariant AMAModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
+ switch (section) {
+ case 0:
+ return QString("Question");
+ case 1:
+ return QString("Accept");
+ case 2:
+ return QString("Reject");
+ }
+ }
+ return QVariant();
+}
+
+void AMAModel::addQuestion(const QString &question, const QString &username)
+{
+ int endIndex = questions.length();
+ beginInsertRows(QModelIndex(), endIndex, endIndex);
+
+ questions.push_back(
+ QSharedPointer(new Question(question, username)));
+ endInsertRows();
+}
+
+QSharedPointer AMAModel::getQuestion(int row)
+{
+ if (row < questions.length() && row >= 0) {
+ return QSharedPointer(questions[row]);
+ }
+ return nullptr;
+}
+
+void AMAModel::removeQuestion(int row)
+{
+ if (questions.length() == 0) {
+ return;
+ }
+ beginRemoveRows(QModelIndex(), row, row);
+ questions.removeAt(row);
+ endRemoveRows();
+}
+
+void AMAModel::clear()
+{
+ if (questions.length() == 0) {
+ return;
+ }
+ beginRemoveRows(QModelIndex(), 0, questions.length() - 1);
+ questions.clear();
+ endRemoveRows();
+}
+
+namespace {
+
+void on_frontend_event(enum obs_frontend_event event, void *param)
+{
+ auto *panel = static_cast(param);
+ if (!panel->IsActive()) {
+ return;
+ }
+
+ switch (event) {
+ case OBS_FRONTEND_EVENT_STREAMING_STARTED:
+ panel->SetPage(PAGE_STREAMING);
+ panel->CreateOverlay();
+ break;
+ case OBS_FRONTEND_EVENT_STREAMING_STOPPING:
+ // TODO: post TOC comment
+ case OBS_FRONTEND_EVENT_STREAMING_STOPPED:
+ panel->SetPage(PAGE_OFFLINE);
+ panel->RemoveUI();
+ case OBS_FRONTEND_EVENT_EXIT:
+ panel->DestroyOverlay();
+ break;
+ case OBS_FRONTEND_EVENT_SCENE_CHANGED:
+ panel->DestroyOverlay();
+ break;
+ }
+}
+
+}
diff --git a/UI/panel-reddit-ama.hpp b/UI/panel-reddit-ama.hpp
new file mode 100644
index 0000000..cb937ca
--- /dev/null
+++ b/UI/panel-reddit-ama.hpp
@@ -0,0 +1,83 @@
+#pragma once
+
+#include
+
+#include "obs.hpp"
+#include "ui_RedditAMAPanel.h"
+
+class AMAModel;
+
+class RedditAMAPanel : public QDockWidget {
+Q_OBJECT
+
+public:
+ static AMAModel *Model() { return model; }
+
+public:
+ explicit RedditAMAPanel(QWidget *parent = nullptr);
+
+ ~RedditAMAPanel();
+
+ void SetPage(int page);
+ OBSSceneItem CreateOverlay();
+ void DestroyOverlay();
+ void RemoveUI();
+ bool IsActive();
+
+ QScopedPointer ui;
+
+private:
+ void HideOverlay();
+ void ClearPanel();
+ void CreateOverlayBackground(OBSScene scene, OBSSceneItem group);
+ void CreateOverlayUsername(OBSScene scene, OBSSceneItem group);
+ void CreateOverlayQuestion(OBSScene scene, OBSSceneItem group);
+ void CreateOverlayLogo(OBSScene scene, OBSSceneItem group);
+
+ static AMAModel *model;
+
+private slots:
+ void OnSelectionChanged(const QModelIndex ¤t,
+ const QModelIndex &previous);
+ void OnDismissQuestion();
+};
+
+class Question {
+public:
+ Question(const QString &q, const QString &u)
+ : question(q),
+ username(u)
+ {
+ }
+
+ QString question;
+ QString username;
+};
+
+class AMAModel : public QAbstractTableModel {
+Q_OBJECT
+
+public:
+ AMAModel(QWidget *parent = nullptr);
+
+ int rowCount(const QModelIndex &) const override
+ {
+ return questions.length();
+ }
+
+ int columnCount(const QModelIndex &) const override { return 3; }
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role) const override;
+
+ void addQuestion(const QString &question, const QString &username);
+ QSharedPointer getQuestion(int row);
+ void removeQuestion(int row);
+ void clear();
+
+private:
+ QList> questions;
+ QScopedPointer acceptIcon;
+ QScopedPointer removeIcon;
+};
diff --git a/UI/panel-reddit-chat.cpp b/UI/panel-reddit-chat.cpp
index c2707ec..c021507 100644
--- a/UI/panel-reddit-chat.cpp
+++ b/UI/panel-reddit-chat.cpp
@@ -9,6 +9,7 @@
#include "api-reddit.hpp"
#include "obs-frontend-api.h"
+#include "panel-reddit-ama.hpp"
#include "qt-wrappers.hpp"
#include "remote-text.hpp"
#include "window-basic-main.hpp"
@@ -124,7 +125,7 @@ void RedditChatPanel::SetPage(int page)
auto *main = OBSBasic::Get();
auto *broadcastId = config_get_string(main->Config(), "Reddit",
- "BroadcastId");
+ "BroadcastId");
auto *thread = RedditApi::FetchStream(broadcastId);
connect(thread, &RemoteTextThread::Result, this,
@@ -204,6 +205,31 @@ void RedditChatPanel::ParseComment(const string &author,
}
}
+ QString qAuthor = QString::fromStdString(author);
+ QString body = QString::fromStdString(blockText).remove(
+ QRegExp("<[^>]*>")).trimmed();
+ bool isQuestion = false;
+ if (config_get_bool(OBSBasic::Get()->Config(), "Reddit",
+ "AMAMode")) {
+ QString bodyLower = body.toLower();
+ if (bodyLower.contains("#ama") ||
+ bodyLower.contains("/ama") ||
+ bodyLower.contains("/q")) {
+ body = body.replace("/ama", "",
+ Qt::CaseInsensitive)
+ .replace(
+ "#ama", "",
+ Qt::CaseInsensitive)
+ .replace(
+ "/q", "",
+ Qt::CaseInsensitive)
+ .trimmed();
+ isQuestion = true;
+ }
+ }
+ if (isQuestion) {
+ RedditAMAPanel::Model()->addQuestion(body, qAuthor);
+ }
blocks.emplace_back(blockText);
}
@@ -267,6 +293,9 @@ void RedditChatPanel::WebsocketError(QAbstractSocket::SocketError error)
{
blog(LOG_ERROR, "Websocket error: (%d) %s", error,
websocket.errorString().toStdString().c_str());
+
+ websocket.close();
+ WebsocketDisconnected();
}
void RedditChatPanel::MessageReceived(QString message)
@@ -420,7 +449,7 @@ void RedditChatPanel::OnLoadStream(const QString &text,
QString RedditChatPanel::GetAvatar(const string &authorId)
{
int id = QString::fromStdString(authorId).split('_')[1].toInt(nullptr,
- 36);
+ 36);
int avatar = (id % avatarCount) + 1;
QString color = avatarColors[(id / avatarCount) % avatarColors.size()];
@@ -445,7 +474,8 @@ void RedditChatPanel::ParseCommand(const QString &comment)
cursor.movePosition(QTextCursor::End);
QTextTable *table = cursor.insertTable(1, 1, tableFormat);
table->cellAt(0, 0).firstCursorPosition().insertHtml(
- QString("Invalid command: %1").arg(comment));
+ QString("Invalid command: %1").arg(
+ comment));
}
}
diff --git a/UI/platform-osx.mm b/UI/platform-osx.mm
index b54b764..7b1f60b 100644
--- a/UI/platform-osx.mm
+++ b/UI/platform-osx.mm
@@ -41,13 +41,13 @@ bool GetDataFilePath(const char *data, string &output)
[NSRunningApplication currentApplication];
NSURL *bundleURL = [app bundleURL];
NSString *path = [NSString
- stringWithFormat:@"Contents/Resources/data/obs-studio/%@",
+ stringWithFormat:@"Contents/Resources/data/rpan-studio/%@",
[NSString stringWithUTF8String:data]];
NSURL *dataURL = [bundleURL URLByAppendingPathComponent:path];
output = [[dataURL path] UTF8String];
} else {
stringstream str;
- str << OBS_DATA_PATH "/obs-studio/" << data;
+ str << OBS_DATA_PATH "/rpan-studio/" << data;
output = str.str();
}
@@ -144,14 +144,29 @@ bool IsAlwaysOnTop(QWidget *window)
return (window->windowFlags() & Qt::WindowStaysOnTopHint) != 0;
}
+void disableColorSpaceConversion(QWidget *window)
+{
+ NSView *view =
+ (__bridge NSView *)reinterpret_cast(window->winId());
+ view.window.colorSpace = NSColorSpace.sRGBColorSpace;
+}
+
void SetAlwaysOnTop(QWidget *window, bool enable)
{
Qt::WindowFlags flags = window->windowFlags();
- if (enable)
+ if (enable) {
+ /* Force the level of the window high so it sits on top of
+ * full-screen applications like Keynote */
+ NSView *nsv = (__bridge NSView *)reinterpret_cast(
+ window->winId());
+ NSWindow *nsw = nsv.window;
+ [nsw setLevel:1024];
+
flags |= Qt::WindowStaysOnTopHint;
- else
+ } else {
flags &= ~Qt::WindowStaysOnTopHint;
+ }
window->setWindowFlags(flags);
window->show();
diff --git a/UI/platform-windows.cpp b/UI/platform-windows.cpp
index 61ac6bc..b4e01d2 100644
--- a/UI/platform-windows.cpp
+++ b/UI/platform-windows.cpp
@@ -53,10 +53,10 @@ static inline bool check_path(const char *data, const char *path,
bool GetDataFilePath(const char *data, string &output)
{
- if (check_path(data, "data/obs-studio/", output))
+ if (check_path(data, "data/" CONFIG_DIR_NAME "/", output))
return true;
- return check_path(data, OBS_DATA_PATH "/obs-studio/", output);
+ return check_path(data, OBS_DATA_PATH "/" CONFIG_DIR_NAME "/", output);
}
bool InitApplicationBundle()
diff --git a/UI/platform-x11.cpp b/UI/platform-x11.cpp
index 3219f6d..332c439 100644
--- a/UI/platform-x11.cpp
+++ b/UI/platform-x11.cpp
@@ -41,7 +41,9 @@ static inline bool check_path(const char *data, const char *path,
return (access(output.c_str(), R_OK) == 0);
}
-#define INSTALL_DATA_PATH OBS_INSTALL_PREFIX OBS_DATA_PATH "/obs-studio/"
+#define INSTALL_DATA_PATH OBS_INSTALL_PREFIX OBS_DATA_PATH "/" CONFIG_DIR_NAME "/"
+// When running from the build rundir
+#define RUNDIR_DATA_PATH OBS_RELATIVE_PREFIX OBS_RELATIVE_PREFIX "data/" CONFIG_DIR_NAME "/"
bool GetDataFilePath(const char *data, string &output)
{
@@ -51,10 +53,12 @@ bool GetDataFilePath(const char *data, string &output)
return true;
}
- if (check_path(data, OBS_DATA_PATH "/obs-studio/", output))
+ if (check_path(data, OBS_DATA_PATH "/" CONFIG_DIR_NAME "/", output))
return true;
if (check_path(data, INSTALL_DATA_PATH, output))
return true;
+ if (check_path(data, RUNDIR_DATA_PATH, output))
+ return true;
return false;
}
@@ -72,6 +76,7 @@ string GetDefaultVideoSavePath()
vector GetPreferredLocales()
{
setlocale(LC_ALL, "");
+ vector matched;
string messages = setlocale(LC_MESSAGES, NULL);
if (!messages.size() || messages == "C" || messages == "POSIX")
return {};
@@ -85,10 +90,10 @@ vector GetPreferredLocales()
return {locale};
if (locale.substr(0, 2) == messages.substr(0, 2))
- return {locale};
+ matched.push_back(locale);
}
- return {};
+ return matched;
}
bool IsAlwaysOnTop(QWidget *window)
diff --git a/UI/platform.hpp b/UI/platform.hpp
index 6100e6f..df21818 100644
--- a/UI/platform.hpp
+++ b/UI/platform.hpp
@@ -67,4 +67,5 @@ QString GetMonitorName(const QString &id);
void EnableOSXVSync(bool enable);
void EnableOSXDockIcon(bool enable);
void InstallNSApplicationSubclass();
+void disableColorSpaceConversion(QWidget *window);
#endif
diff --git a/UI/properties-view.cpp b/UI/properties-view.cpp
index 350cf47..849dd48 100644
--- a/UI/properties-view.cpp
+++ b/UI/properties-view.cpp
@@ -114,8 +114,6 @@ void OBSPropertiesView::RefreshProperties()
widget->setLayout(layout);
QSizePolicy mainPolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
- QSizePolicy policy(QSizePolicy::Preferred, QSizePolicy::Preferred);
- //widget->setSizePolicy(policy);
layout->setLabelAlignment(Qt::AlignRight);
@@ -596,6 +594,13 @@ void OBSPropertiesView::AddEditableList(obs_property_t *prop,
WidgetInfo *info = new WidgetInfo(this, prop, list);
+ list->setDragDropMode(QAbstractItemView::InternalMove);
+ connect(list->model(),
+ SIGNAL(rowsMoved(QModelIndex, int, int, QModelIndex, int)),
+ info,
+ SLOT(EditListReordered(const QModelIndex &, int, int,
+ const QModelIndex &, int)));
+
QVBoxLayout *sideLayout = new QVBoxLayout();
NewButton(sideLayout, info, "addIconSmall", &WidgetInfo::EditListAdd);
NewButton(sideLayout, info, "removeIconSmall",
@@ -652,14 +657,15 @@ void OBSPropertiesView::AddColor(obs_property_t *prop, QFormLayout *layout,
QPalette palette = QPalette(color);
colorLabel->setFrameStyle(QFrame::Sunken | QFrame::Panel);
- colorLabel->setText(color.name(QColor::HexArgb));
+ // The picker doesn't have an alpha option, show only RGB
+ colorLabel->setText(color.name(QColor::HexRgb));
colorLabel->setPalette(palette);
colorLabel->setStyleSheet(
QString("background-color :%1; color: %2;")
.arg(palette.color(QPalette::Window)
- .name(QColor::HexArgb))
+ .name(QColor::HexRgb))
.arg(palette.color(QPalette::WindowText)
- .name(QColor::HexArgb)));
+ .name(QColor::HexRgb)));
colorLabel->setAutoFillBackground(true);
colorLabel->setAlignment(Qt::AlignCenter);
colorLabel->setToolTip(QT_UTF8(obs_property_long_description(prop)));
@@ -678,7 +684,7 @@ void OBSPropertiesView::AddColor(obs_property_t *prop, QFormLayout *layout,
layout->addRow(label, subLayout);
}
-static void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false)
+void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false)
{
const char *face = obs_data_get_string(font_obj, "face");
const char *style = obs_data_get_string(font_obj, "style");
@@ -1437,28 +1443,42 @@ void OBSPropertiesView::AddProperty(obs_property_t *property,
return;
if (obs_property_long_description(property)) {
- QString lStr = "%1 ";
bool lightTheme = palette().text().color().redF() < 0.5;
QString file = lightTheme ? ":/res/images/help.svg"
: ":/res/images/help_light.svg";
if (label) {
+ QString lStr = "%1 ";
+
label->setText(lStr.arg(label->text(), file));
label->setToolTip(
obs_property_long_description(property));
} else if (type == OBS_PROPERTY_BOOL) {
+
+ QString bStr = " ";
+
+ const char *desc = obs_property_description(property);
+
QWidget *newWidget = new QWidget();
+
QHBoxLayout *boxLayout = new QHBoxLayout(newWidget);
boxLayout->setContentsMargins(0, 0, 0, 0);
boxLayout->setAlignment(Qt::AlignLeft);
+ boxLayout->setSpacing(0);
QCheckBox *check = qobject_cast(widget);
- QLabel *help =
- new QLabel(lStr.arg(check->text(), file));
+ check->setText(desc);
+ check->setToolTip(
+ obs_property_long_description(property));
+
+ QLabel *help = new QLabel(check);
+ help->setText(bStr.arg(file));
help->setToolTip(
obs_property_long_description(property));
- check->setText("");
+
boxLayout->addWidget(check);
boxLayout->addWidget(help);
widget = newWidget;
@@ -1625,18 +1645,14 @@ bool WidgetInfo::PathChanged(const char *setting)
QString path;
if (type == OBS_PATH_DIRECTORY)
- path = QFileDialog::getExistingDirectory(
- view, QT_UTF8(desc), QT_UTF8(default_path),
- QFileDialog::ShowDirsOnly |
- QFileDialog::DontResolveSymlinks);
+ path = SelectDirectory(view, QT_UTF8(desc),
+ QT_UTF8(default_path));
else if (type == OBS_PATH_FILE)
- path = QFileDialog::getOpenFileName(view, QT_UTF8(desc),
- QT_UTF8(default_path),
- QT_UTF8(filter));
+ path = OpenFile(view, QT_UTF8(desc), QT_UTF8(default_path),
+ QT_UTF8(filter));
else if (type == OBS_PATH_FILE_SAVE)
- path = QFileDialog::getSaveFileName(view, QT_UTF8(desc),
- QT_UTF8(default_path),
- QT_UTF8(filter));
+ path = SaveFile(view, QT_UTF8(desc), QT_UTF8(default_path),
+ QT_UTF8(filter));
if (path.isEmpty())
return false;
@@ -1688,7 +1704,7 @@ bool WidgetInfo::ColorChanged(const char *setting)
long long val = obs_data_get_int(view->settings, setting);
QColor color = color_from_int(val);
- QColorDialog::ColorDialogOptions options = 0;
+ QColorDialog::ColorDialogOptions options;
/* The native dialog on OSX has all kinds of problems, like closing
* other open QDialogs on exit, and
@@ -1705,14 +1721,14 @@ bool WidgetInfo::ColorChanged(const char *setting)
return false;
QLabel *label = static_cast(widget);
- label->setText(color.name(QColor::HexArgb));
+ label->setText(color.name(QColor::HexRgb));
QPalette palette = QPalette(color);
label->setPalette(palette);
label->setStyleSheet(QString("background-color :%1; color: %2;")
.arg(palette.color(QPalette::Window)
- .name(QColor::HexArgb))
+ .name(QColor::HexRgb))
.arg(palette.color(QPalette::WindowText)
- .name(QColor::HexArgb)));
+ .name(QColor::HexRgb)));
obs_data_set_int(view->settings, setting, color_to_int(color));
@@ -1776,6 +1792,19 @@ void WidgetInfo::GroupChanged(const char *setting)
: true);
}
+void WidgetInfo::EditListReordered(const QModelIndex &parent, int start,
+ int end, const QModelIndex &destination,
+ int row)
+{
+ UNUSED_PARAMETER(parent);
+ UNUSED_PARAMETER(start);
+ UNUSED_PARAMETER(end);
+ UNUSED_PARAMETER(destination);
+ UNUSED_PARAMETER(row);
+
+ EditableListChanged();
+}
+
void WidgetInfo::EditableListChanged()
{
const char *setting = obs_property_name(property);
@@ -1886,9 +1915,8 @@ class EditableItemDialog : public QDialog {
if (curPath.isEmpty())
curPath = default_path;
- QString path = QFileDialog::getOpenFileName(
- App()->GetMainWindow(), QTStr("Browse"), curPath,
- filter);
+ QString path = OpenFile(App()->GetMainWindow(), QTStr("Browse"),
+ curPath, filter);
if (path.isEmpty())
return;
@@ -2007,9 +2035,8 @@ void WidgetInfo::EditListAddFiles()
QString title = QTStr("Basic.PropertiesWindow.AddEditableListFiles")
.arg(QT_UTF8(desc));
- QStringList files = QFileDialog::getOpenFileNames(
- App()->GetMainWindow(), title, QT_UTF8(default_path),
- QT_UTF8(filter));
+ QStringList files = OpenFiles(App()->GetMainWindow(), title,
+ QT_UTF8(default_path), QT_UTF8(filter));
if (files.count() == 0)
return;
@@ -2028,8 +2055,8 @@ void WidgetInfo::EditListAddDir()
QString title = QTStr("Basic.PropertiesWindow.AddEditableListDir")
.arg(QT_UTF8(desc));
- QString dir = QFileDialog::getExistingDirectory(
- App()->GetMainWindow(), title, QT_UTF8(default_path));
+ QString dir = SelectDirectory(App()->GetMainWindow(), title,
+ QT_UTF8(default_path));
if (dir.isEmpty())
return;
@@ -2067,15 +2094,11 @@ void WidgetInfo::EditListEdit()
QString path;
if (pathDir.exists())
- path = QFileDialog::getExistingDirectory(
- App()->GetMainWindow(), QTStr("Browse"),
- item->text(),
- QFileDialog::ShowDirsOnly |
- QFileDialog::DontResolveSymlinks);
+ path = SelectDirectory(App()->GetMainWindow(),
+ QTStr("Browse"), item->text());
else
- path = QFileDialog::getOpenFileName(
- App()->GetMainWindow(), QTStr("Browse"),
- item->text(), QT_UTF8(filter));
+ path = OpenFile(App()->GetMainWindow(), QTStr("Browse"),
+ item->text(), QT_UTF8(filter));
if (path.isEmpty())
return;
diff --git a/UI/properties-view.hpp b/UI/properties-view.hpp
index 892c454..169c937 100644
--- a/UI/properties-view.hpp
+++ b/UI/properties-view.hpp
@@ -58,6 +58,8 @@ public slots:
void EditListEdit();
void EditListUp();
void EditListDown();
+ void EditListReordered(const QModelIndex &parent, int start, int end,
+ const QModelIndex &destination, int row);
};
/* ------------------------------------------------------------------------- */
diff --git a/UI/qt-display.hpp b/UI/qt-display.hpp
index a2e5a3e..816d96b 100644
--- a/UI/qt-display.hpp
+++ b/UI/qt-display.hpp
@@ -24,7 +24,7 @@ class OBSQTDisplay : public QWidget {
public:
OBSQTDisplay(QWidget *parent = nullptr,
- Qt::WindowFlags flags = nullptr);
+ Qt::WindowFlags flags = Qt::WindowFlags());
virtual QPaintEngine *paintEngine() const override;
diff --git a/UI/qt-wrappers.cpp b/UI/qt-wrappers.cpp
index e8a51d3..b9d00a7 100644
--- a/UI/qt-wrappers.cpp
+++ b/UI/qt-wrappers.cpp
@@ -25,6 +25,7 @@
#include
#include
#include
+#include
#if !defined(_WIN32) && !defined(__APPLE__)
#include
@@ -168,6 +169,7 @@ QDataStream &operator>>(QDataStream &in, OBSScene &scene)
obs_source_t *source = obs_get_source_by_name(QT_TO_UTF8(sceneName));
scene = obs_scene_from_source(source);
+ obs_source_release(source);
return in;
}
@@ -264,7 +266,7 @@ void ExecuteFuncSafeBlockMsgBox(std::function func,
dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowCloseButtonHint);
dlg.setWindowTitle(title);
dlg.setText(text);
- dlg.setStandardButtons(0);
+ dlg.setStandardButtons(QMessageBox::StandardButtons());
auto wait = [&]() {
func();
@@ -335,3 +337,64 @@ void setThemeID(QWidget *widget, const QString &themeID)
widget->setStyleSheet(qss);
}
}
+
+QString SelectDirectory(QWidget *parent, QString title, QString path)
+{
+#if defined(BROWSER_AVAILABLE) && defined(__linux__)
+ QString dir = QFileDialog::getExistingDirectory(
+ parent, title, path,
+ QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks |
+ QFileDialog::DontUseNativeDialog);
+#else
+ QString dir = QFileDialog::getExistingDirectory(
+ parent, title, path,
+ QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
+#endif
+
+ return dir;
+}
+
+QString SaveFile(QWidget *parent, QString title, QString path,
+ QString extensions)
+{
+#if defined(BROWSER_AVAILABLE) && defined(__linux__)
+ QString file = QFileDialog::getSaveFileName(
+ parent, title, path, extensions, nullptr,
+ QFileDialog::DontUseNativeDialog);
+#else
+ QString file =
+ QFileDialog::getSaveFileName(parent, title, path, extensions);
+#endif
+
+ return file;
+}
+
+QString OpenFile(QWidget *parent, QString title, QString path,
+ QString extensions)
+{
+#if defined(BROWSER_AVAILABLE) && defined(__linux__)
+ QString file = QFileDialog::getOpenFileName(
+ parent, title, path, extensions, nullptr,
+ QFileDialog::DontUseNativeDialog);
+#else
+ QString file =
+ QFileDialog::getOpenFileName(parent, title, path, extensions);
+#endif
+
+ return file;
+}
+
+QStringList OpenFiles(QWidget *parent, QString title, QString path,
+ QString extensions)
+{
+#if defined(BROWSER_AVAILABLE) && defined(__linux__)
+ QStringList files = QFileDialog::getOpenFileNames(
+ parent, title, path, extensions, nullptr,
+ QFileDialog::DontUseNativeDialog);
+#else
+ QStringList files =
+ QFileDialog::getOpenFileNames(parent, title, path, extensions);
+#endif
+
+ return files;
+}
diff --git a/UI/qt-wrappers.hpp b/UI/qt-wrappers.hpp
index 69996de..71b1135 100644
--- a/UI/qt-wrappers.hpp
+++ b/UI/qt-wrappers.hpp
@@ -107,3 +107,11 @@ bool LineEditCanceled(QEvent *event);
bool LineEditChanged(QEvent *event);
void setThemeID(QWidget *widget, const QString &themeID);
+
+QString SelectDirectory(QWidget *parent, QString title, QString path);
+QString SaveFile(QWidget *parent, QString title, QString path,
+ QString extensions);
+QString OpenFile(QWidget *parent, QString title, QString path,
+ QString extensions);
+QStringList OpenFiles(QWidget *parent, QString title, QString path,
+ QString extensions);
diff --git a/UI/scene-tree.cpp b/UI/scene-tree.cpp
index 801e83c..2111d9e 100644
--- a/UI/scene-tree.cpp
+++ b/UI/scene-tree.cpp
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
SceneTree::SceneTree(QWidget *parent_) : QListWidget(parent_)
{
@@ -67,8 +68,6 @@ bool SceneTree::eventFilter(QObject *obj, QEvent *event)
void SceneTree::resizeEvent(QResizeEvent *event)
{
- QListWidget::resizeEvent(event);
-
if (gridMode) {
int scrollWid = verticalScrollBar()->sizeHint().width();
int h = visualItemRect(item(count() - 1)).bottom();
@@ -96,6 +95,8 @@ void SceneTree::resizeEvent(QResizeEvent *event)
item(i)->setData(Qt::SizeHintRole, QVariant());
}
}
+
+ QListWidget::resizeEvent(event);
}
void SceneTree::startDrag(Qt::DropActions supportedActions)
@@ -105,8 +106,12 @@ void SceneTree::startDrag(Qt::DropActions supportedActions)
void SceneTree::dropEvent(QDropEvent *event)
{
- QListWidget::dropEvent(event);
- if (event->source() == this && gridMode) {
+ if (event->source() != this) {
+ QListWidget::dropEvent(event);
+ return;
+ }
+
+ if (gridMode) {
int scrollWid = verticalScrollBar()->sizeHint().width();
int h = visualItemRect(item(count() - 1)).bottom();
@@ -131,6 +136,10 @@ void SceneTree::dropEvent(QDropEvent *event)
setCurrentItem(item);
resize(size());
}
+
+ QListWidget::dropEvent(event);
+
+ QTimer::singleShot(100, [this]() { emit scenesReordered(); });
}
void SceneTree::dragMoveEvent(QDragMoveEvent *event)
@@ -175,15 +184,15 @@ void SceneTree::dragMoveEvent(QDragMoveEvent *event)
QPoint position(xPos * g.width(), yPos * g.height());
setPositionForIndex(position, index);
}
- } else {
- QListWidget::dragMoveEvent(event);
}
+
+ QListWidget::dragMoveEvent(event);
}
void SceneTree::rowsInserted(const QModelIndex &parent, int start, int end)
{
- QListWidget::rowsInserted(parent, start, end);
-
QResizeEvent event(size(), size());
SceneTree::resizeEvent(&event);
+
+ QListWidget::rowsInserted(parent, start, end);
}
diff --git a/UI/scene-tree.hpp b/UI/scene-tree.hpp
index 4b04dda..99090eb 100644
--- a/UI/scene-tree.hpp
+++ b/UI/scene-tree.hpp
@@ -34,4 +34,7 @@ class SceneTree : public QListWidget {
virtual void dragMoveEvent(QDragMoveEvent *event) override;
virtual void rowsInserted(const QModelIndex &parent, int start,
int end) override;
+
+signals:
+ void scenesReordered();
};
diff --git a/UI/screenshot-obj.hpp b/UI/screenshot-obj.hpp
new file mode 100644
index 0000000..df7c148
--- /dev/null
+++ b/UI/screenshot-obj.hpp
@@ -0,0 +1,47 @@
+/******************************************************************************
+ Copyright (C) 2020 by Hugh Bailey
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+******************************************************************************/
+
+#include
+#include
+#include
+#include
+
+class ScreenshotObj : public QObject {
+ Q_OBJECT
+
+public:
+ ScreenshotObj(obs_source_t *source);
+ ~ScreenshotObj() override;
+ void Screenshot();
+ void Download();
+ void Copy();
+ void MuxAndFinish();
+
+ gs_texrender_t *texrender = nullptr;
+ gs_stagesurf_t *stagesurf = nullptr;
+ OBSWeakSource weakSource;
+ std::string path;
+ QImage image;
+ uint32_t cx;
+ uint32_t cy;
+ std::thread th;
+
+ int stage = 0;
+
+public slots:
+ void Save();
+};
diff --git a/UI/source-label.hpp b/UI/source-label.hpp
index 4a629b8..b8f0fbe 100644
--- a/UI/source-label.hpp
+++ b/UI/source-label.hpp
@@ -29,7 +29,7 @@ class OBSSourceLabel : public QLabel {
OBSSignal destroyedSignal;
OBSSourceLabel(const obs_source_t *source, QWidget *parent = nullptr,
- Qt::WindowFlags f = 0)
+ Qt::WindowFlags f = Qt::WindowFlags())
: QLabel(obs_source_get_name(source), parent, f),
renamedSignal(obs_source_get_signal_handler(source), "rename",
&OBSSourceLabel::SourceRenamed, this),
diff --git a/UI/source-tree.cpp b/UI/source-tree.cpp
index fdd4c37..103cddc 100644
--- a/UI/source-tree.cpp
+++ b/UI/source-tree.cpp
@@ -151,6 +151,7 @@ void SourceTreeItem::DisconnectSignals()
{
sceneRemoveSignal.Disconnect();
itemRemoveSignal.Disconnect();
+ selectSignal.Disconnect();
deselectSignal.Disconnect();
visibleSignal.Disconnect();
lockedSignal.Disconnect();
@@ -212,6 +213,16 @@ void SourceTreeItem::ReconnectSignals()
Q_ARG(bool, locked));
};
+ auto itemSelect = [](void *data, calldata_t *cd) {
+ SourceTreeItem *this_ =
+ reinterpret_cast(data);
+ obs_sceneitem_t *curItem =
+ (obs_sceneitem_t *)calldata_ptr(cd, "item");
+
+ if (curItem == this_->sceneitem)
+ QMetaObject::invokeMethod(this_, "Select");
+ };
+
auto itemDeselect = [](void *data, calldata_t *cd) {
SourceTreeItem *this_ =
reinterpret_cast(data);
@@ -236,6 +247,8 @@ void SourceTreeItem::ReconnectSignals()
itemRemoveSignal.Connect(signal, "item_remove", removeItem, this);
visibleSignal.Connect(signal, "item_visible", itemVisible, this);
lockedSignal.Connect(signal, "item_locked", itemLocked, this);
+ selectSignal.Connect(signal, "item_select", itemSelect, this);
+ deselectSignal.Connect(signal, "item_deselect", itemDeselect, this);
if (obs_sceneitem_is_group(sceneitem)) {
obs_source_t *source = obs_sceneitem_get_source(sceneitem);
@@ -245,10 +258,6 @@ void SourceTreeItem::ReconnectSignals()
this);
}
- if (scene != GetCurrentScene())
- deselectSignal.Connect(signal, "item_deselect", itemDeselect,
- this);
-
/* --------------------------------------------------------- */
auto renamed = [](void *data, calldata_t *cd) {
@@ -516,9 +525,16 @@ void SourceTreeItem::ExpandClicked(bool checked)
tree->GetStm()->CollapseGroup(sceneitem);
}
+void SourceTreeItem::Select()
+{
+ tree->SelectItem(sceneitem, true);
+ OBSBasic::Get()->UpdateContextBar();
+}
+
void SourceTreeItem::Deselect()
{
tree->SelectItem(sceneitem, false);
+ OBSBasic::Get()->UpdateContextBar();
}
/* ========================================================================= */
@@ -566,7 +582,13 @@ static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr)
obs_data_release(data);
}
- items.insert(0, item);
+ obs_data_t *data = obs_source_get_settings(obs_sceneitem_get_source(item));
+ bool hidden = obs_data_get_bool(data, "hidden");
+ obs_data_release(data);
+
+ if(!hidden) {
+ items.insert(0, item);
+ }
return true;
}
diff --git a/UI/source-tree.hpp b/UI/source-tree.hpp
index 32a4c59..329108f 100644
--- a/UI/source-tree.hpp
+++ b/UI/source-tree.hpp
@@ -70,6 +70,7 @@ class SourceTreeItem : public QWidget {
OBSSignal sceneRemoveSignal;
OBSSignal itemRemoveSignal;
OBSSignal groupReorderSignal;
+ OBSSignal selectSignal;
OBSSignal deselectSignal;
OBSSignal visibleSignal;
OBSSignal lockedSignal;
@@ -90,6 +91,7 @@ private slots:
void ExpandClicked(bool checked);
+ void Select();
void Deselect();
};
diff --git a/UI/ui-config.h.in b/UI/ui-config.h.in
index d754a06..855ed49 100644
--- a/UI/ui-config.h.in
+++ b/UI/ui-config.h.in
@@ -20,10 +20,6 @@
#define TWITCH_CLIENTID "@TWITCH_CLIENTID@"
#define TWITCH_HASH 0x@TWITCH_HASH@
-#define MIXER_ENABLED @MIXER_ENABLED@
-#define MIXER_CLIENTID "@MIXER_CLIENTID@"
-#define MIXER_HASH 0x@MIXER_HASH@
-
#define RESTREAM_ENABLED @RESTREAM_ENABLED@
#define RESTREAM_CLIENTID "@RESTREAM_CLIENTID@"
#define RESTREAM_HASH 0x@RESTREAM_HASH@
diff --git a/UI/visibility-checkbox.cpp b/UI/visibility-checkbox.cpp
new file mode 100644
index 0000000..85cce6f
--- /dev/null
+++ b/UI/visibility-checkbox.cpp
@@ -0,0 +1,5 @@
+#include "visibility-checkbox.hpp"
+
+VisibilityCheckBox::VisibilityCheckBox() {}
+
+VisibilityCheckBox::VisibilityCheckBox(QWidget *parent) : QCheckBox(parent) {}
diff --git a/UI/visibility-checkbox.hpp b/UI/visibility-checkbox.hpp
index ff21df2..e2f1f62 100644
--- a/UI/visibility-checkbox.hpp
+++ b/UI/visibility-checkbox.hpp
@@ -4,4 +4,8 @@
class VisibilityCheckBox : public QCheckBox {
Q_OBJECT
+
+public:
+ VisibilityCheckBox();
+ explicit VisibilityCheckBox(QWidget *parent);
};
diff --git a/UI/volume-control.cpp b/UI/volume-control.cpp
index c491a6f..7543d15 100644
--- a/UI/volume-control.cpp
+++ b/UI/volume-control.cpp
@@ -531,6 +531,8 @@ VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter,
bool vertical)
: QWidget(parent), obs_volmeter(obs_volmeter), vertical(vertical)
{
+ setAttribute(Qt::WA_OpaquePaintEvent, true);
+
// Use a font that can be rendered small.
tickFont = QFont("Arial");
tickFont.setPixelSize(7);
@@ -562,7 +564,8 @@ VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter,
updateTimerRef = updateTimer.toStrongRef();
if (!updateTimerRef) {
updateTimerRef = QSharedPointer::create();
- updateTimerRef->start(34);
+ updateTimerRef->setTimerType(Qt::PreciseTimer);
+ updateTimerRef->start(16);
updateTimer = updateTimerRef;
}
@@ -875,7 +878,7 @@ void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width,
painter.fillRect(peakPosition, y,
maximumPosition - peakPosition, height,
backgroundErrorColor);
- } else {
+ } else if (int(magnitude) != 0) {
if (!clipping) {
QTimer::singleShot(CLIP_FLASH_DURATION_MS, this,
SLOT(ClipEnding()));
@@ -1040,6 +1043,11 @@ void VolumeMeter::paintEvent(QPaintEvent *event)
// Actual painting of the widget starts here.
QPainter painter(this);
+
+ // Paint window background color (as widget is opaque)
+ QColor background = palette().color(QPalette::ColorRole::Window);
+ painter.fillRect(rect, background);
+
if (vertical) {
// Invert the Y axis to ease the math
painter.translate(0, height);
diff --git a/UI/win-update/updater/CMakeLists.txt b/UI/win-update/updater/CMakeLists.txt
index a65ceab..83309c3 100644
--- a/UI/win-update/updater/CMakeLists.txt
+++ b/UI/win-update/updater/CMakeLists.txt
@@ -14,8 +14,6 @@ include_directories(${LIBLZMA_INCLUDE_DIRS})
include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs")
include_directories(${BLAKE2_INCLUDE_DIR})
-find_package(ZLIB REQUIRED)
-
set(updater_HEADERS
../win-update-helpers.hpp
resource.h
@@ -51,3 +49,4 @@ target_link_libraries(updater
shell32
winhttp
)
+set_target_properties(updater PROPERTIES FOLDER "frontend")
diff --git a/UI/win-update/updater/hash.cpp b/UI/win-update/updater/hash.cpp
index 936d84b..baa2ba5 100644
--- a/UI/win-update/updater/hash.cpp
+++ b/UI/win-update/updater/hash.cpp
@@ -45,28 +45,28 @@ void StringToHash(const wchar_t *in, BYTE *out)
bool CalculateFileHash(const wchar_t *path, BYTE *hash)
{
+ static __declspec(thread) vector hashBuffer;
blake2b_state blake2;
if (blake2b_init(&blake2, BLAKE2_HASH_LENGTH) != 0)
return false;
+ hashBuffer.resize(1048576);
+
WinHandle handle = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, 0, nullptr);
if (handle == INVALID_HANDLE_VALUE)
return false;
- vector buf;
- buf.resize(65536);
-
for (;;) {
DWORD read = 0;
- if (!ReadFile(handle, buf.data(), (DWORD)buf.size(), &read,
+ if (!ReadFile(handle, &hashBuffer[0], hashBuffer.size(), &read,
nullptr))
return false;
if (!read)
break;
- if (blake2b_update(&blake2, buf.data(), read) != 0)
+ if (blake2b_update(&blake2, &hashBuffer[0], read) != 0)
return false;
}
diff --git a/UI/win-update/updater/updater.cpp b/UI/win-update/updater/updater.cpp
index 564b32b..21f566c 100644
--- a/UI/win-update/updater/updater.cpp
+++ b/UI/win-update/updater/updater.cpp
@@ -1153,20 +1153,34 @@ static bool Update(wchar_t *cmdLine)
GetCurrentDirectory(_countof(lpAppDataPath), lpAppDataPath);
StringCbCat(lpAppDataPath, sizeof(lpAppDataPath), L"\\config");
} else {
- CoTaskMemPtr pOut;
- HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData,
- KF_FLAG_DEFAULT, nullptr,
- &pOut);
- if (hr != S_OK) {
+ DWORD ret;
+ ret = GetEnvironmentVariable(L"OBS_USER_APPDATA_PATH",
+ lpAppDataPath,
+ _countof(lpAppDataPath));
+
+ if (ret >= _countof(lpAppDataPath)) {
Status(L"Update failed: Could not determine AppData "
L"location");
return false;
}
- StringCbCopy(lpAppDataPath, sizeof(lpAppDataPath), pOut);
+ if (!ret) {
+ CoTaskMemPtr pOut;
+ HRESULT hr = SHGetKnownFolderPath(
+ FOLDERID_RoamingAppData, KF_FLAG_DEFAULT,
+ nullptr, &pOut);
+ if (hr != S_OK) {
+ Status(L"Update failed: Could not determine AppData "
+ L"location");
+ return false;
+ }
+
+ StringCbCopy(lpAppDataPath, sizeof(lpAppDataPath),
+ pOut);
+ }
}
- StringCbCat(lpAppDataPath, sizeof(lpAppDataPath), L"\\obs-studio");
+ StringCbCat(lpAppDataPath, sizeof(lpAppDataPath), L"\\" CONFIG_DIR_NAME);
/* ------------------------------------- *
* Get download path */
@@ -1184,7 +1198,7 @@ static bool Update(wchar_t *cmdLine)
GetLastError());
return false;
}
- if (!GetTempFileNameW(tempDirName, L"obs-studio", 0, tempPath)) {
+ if (!GetTempFileNameW(tempDirName, CONFIG_DIR_NAME, 0, tempPath)) {
Status(L"Update failed: Failed to create temp dir name: %ld",
GetLastError());
return false;
@@ -1410,6 +1424,57 @@ static bool Update(wchar_t *cmdLine)
}
}
+ /* ------------------------------------- *
+ * Install virtual camera */
+
+ auto runcommand = [](wchar_t *cmd) {
+ STARTUPINFO si = {};
+ si.cb = sizeof(si);
+ si.dwFlags = STARTF_USESHOWWINDOW;
+ si.wShowWindow = SW_HIDE;
+
+ PROCESS_INFORMATION pi;
+ bool success = !!CreateProcessW(nullptr, cmd, nullptr, nullptr,
+ false, CREATE_NEW_CONSOLE,
+ nullptr, nullptr, &si, &pi);
+ if (success) {
+ WaitForSingleObject(pi.hProcess, INFINITE);
+ CloseHandle(pi.hThread);
+ CloseHandle(pi.hProcess);
+ }
+ };
+
+ if (!bIsPortable) {
+ wchar_t regsvr[MAX_PATH];
+ wchar_t src[MAX_PATH];
+ wchar_t tmp[MAX_PATH];
+ wchar_t tmp2[MAX_PATH];
+
+ SHGetFolderPathW(nullptr, CSIDL_SYSTEM, nullptr,
+ SHGFP_TYPE_CURRENT, regsvr);
+ StringCbCat(regsvr, sizeof(regsvr), L"\\regsvr32.exe");
+
+ GetCurrentDirectoryW(_countof(src), src);
+ StringCbCat(src, sizeof(src),
+ L"\\data\\obs-plugins\\win-dshow\\");
+
+ StringCbCopy(tmp, sizeof(tmp), L"\"");
+ StringCbCat(tmp, sizeof(tmp), regsvr);
+ StringCbCat(tmp, sizeof(tmp), L"\" /s \"");
+ StringCbCat(tmp, sizeof(tmp), src);
+ StringCbCat(tmp, sizeof(tmp), L"obs-virtualcam-module");
+
+ StringCbCopy(tmp2, sizeof(tmp2), tmp);
+ StringCbCat(tmp2, sizeof(tmp2), L"32.dll\"");
+ runcommand(tmp2);
+
+ if (is_64bit_windows()) {
+ StringCbCopy(tmp2, sizeof(tmp2), tmp);
+ StringCbCat(tmp2, sizeof(tmp2), L"64.dll\"");
+ runcommand(tmp2);
+ }
+ }
+
/* ------------------------------------- *
* Update hook files and vulkan registry */
@@ -1563,16 +1628,13 @@ static INT_PTR CALLBACK UpdateDialogProc(HWND hwnd, UINT message, WPARAM wParam,
return false;
}
-static void RestartAsAdmin(LPWSTR lpCmdLine)
+static int RestartAsAdmin(LPCWSTR lpCmdLine, LPCWSTR cwd)
{
wchar_t myPath[MAX_PATH];
if (!GetModuleFileNameW(nullptr, myPath, _countof(myPath) - 1)) {
- return;
+ return 0;
}
- wchar_t cwd[MAX_PATH];
- GetCurrentDirectoryW(_countof(cwd) - 1, cwd);
-
SHELLEXECUTEINFO shExInfo = {0};
shExInfo.cbSize = sizeof(shExInfo);
shExInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
@@ -1588,16 +1650,27 @@ static void RestartAsAdmin(LPWSTR lpCmdLine)
* windows :( */
AllowSetForegroundWindow(ASFW_ANY);
+ /* if the admin is a different user, save the path to the user's
+ * appdata so we can load the correct manifest */
+ CoTaskMemPtr pOut;
+ HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData,
+ KF_FLAG_DEFAULT, nullptr, &pOut);
+ if (hr == S_OK)
+ SetEnvironmentVariable(L"OBS_USER_APPDATA_PATH", pOut);
+
if (ShellExecuteEx(&shExInfo)) {
DWORD exitCode;
+ WaitForSingleObject(shExInfo.hProcess, INFINITE);
+
if (GetExitCodeProcess(shExInfo.hProcess, &exitCode)) {
if (exitCode == 1) {
- LaunchOBS();
+ return exitCode;
}
}
CloseHandle(shExInfo.hProcess);
}
+ return 0;
}
static bool HasElevation()
@@ -1622,11 +1695,36 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
{
INITCOMMONCONTROLSEX icce;
+ wchar_t cwd[MAX_PATH];
+ wchar_t newPath[MAX_PATH];
+ GetCurrentDirectoryW(_countof(cwd) - 1, cwd);
+
+ is32bit = wcsstr(cwd, L"bin\\32bit") != nullptr;
+
if (!HasElevation()) {
+
+ WinHandle hMutex = OpenMutex(
+ SYNCHRONIZE, false, L"OBSUpdaterRunningAsNonAdminUser");
+ if (hMutex) {
+ MessageBox(
+ nullptr, L"Updater Error",
+ L"OBS Studio Updater must be run as an administrator.",
+ MB_ICONWARNING);
+ return 2;
+ }
+
HANDLE hLowMutex = CreateMutexW(
nullptr, true, L"OBSUpdaterRunningAsNonAdminUser");
- RestartAsAdmin(lpCmdLine);
+ /* return code 1 = user wanted to launch OBS */
+ if (RestartAsAdmin(lpCmdLine, cwd) == 1) {
+ StringCbCat(cwd, sizeof(cwd), L"\\..\\..");
+ GetFullPathName(cwd, _countof(newPath), newPath,
+ nullptr);
+ SetCurrentDirectory(newPath);
+
+ LaunchOBS();
+ }
if (hLowMutex) {
ReleaseMutex(hLowMutex);
@@ -1635,18 +1733,9 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
return 0;
} else {
- {
- wchar_t cwd[MAX_PATH];
- wchar_t newPath[MAX_PATH];
- GetCurrentDirectoryW(_countof(cwd) - 1, cwd);
-
- is32bit = wcsstr(cwd, L"bin\\32bit") != nullptr;
- StringCbCat(cwd, sizeof(cwd), L"\\..\\..");
-
- GetFullPathName(cwd, _countof(newPath), newPath,
- nullptr);
- SetCurrentDirectory(newPath);
- }
+ StringCbCat(cwd, sizeof(cwd), L"\\..\\..");
+ GetFullPathName(cwd, _countof(newPath), newPath, nullptr);
+ SetCurrentDirectory(newPath);
hinstMain = hInstance;
diff --git a/UI/win-update/win-update.cpp b/UI/win-update/win-update.cpp
index 5d22a0f..bb6ff61 100644
--- a/UI/win-update/win-update.cpp
+++ b/UI/win-update/win-update.cpp
@@ -274,14 +274,14 @@ static bool VerifyDigitalSignature(uint8_t *buf, size_t len, uint8_t *sig,
if (!CryptDecodeObjectEx(X509_ASN_ENCODING, X509_PUBLIC_KEY_INFO,
binaryKey, binaryKeyLen,
- CRYPT_ENCODE_ALLOC_FLAG, nullptr, &publicPBLOB,
+ CRYPT_DECODE_ALLOC_FLAG, nullptr, &publicPBLOB,
&iPBLOBSize))
return false;
if (!CryptDecodeObjectEx(X509_ASN_ENCODING, RSA_CSP_PUBLICKEYBLOB,
publicPBLOB->PublicKey.pbData,
publicPBLOB->PublicKey.cbData,
- CRYPT_ENCODE_ALLOC_FLAG, nullptr,
+ CRYPT_DECODE_ALLOC_FLAG, nullptr,
&rsaPublicBLOB, &rsaPublicBLOBSize))
return false;
diff --git a/UI/window-basic-adv-audio.cpp b/UI/window-basic-adv-audio.cpp
index e95ef9f..2918cac 100644
--- a/UI/window-basic-adv-audio.cpp
+++ b/UI/window-basic-adv-audio.cpp
@@ -26,6 +26,28 @@ OBSBasicAdvAudio::OBSBasicAdvAudio(QWidget *parent)
QWidget *widget;
QLabel *label;
+ QLabel *volLabel = new QLabel(QTStr("Basic.AdvAudio.Volume"));
+ volLabel->setStyleSheet("font-weight: bold;");
+ volLabel->setContentsMargins(0, 0, 6, 0);
+
+ usePercent = new QCheckBox();
+ usePercent->setStyleSheet("font-weight: bold;");
+ usePercent->setText("%");
+ connect(usePercent, SIGNAL(toggled(bool)), this,
+ SLOT(SetVolumeType(bool)));
+
+ VolumeType volType = (VolumeType)config_get_int(
+ GetGlobalConfig(), "BasicWindow", "AdvAudioVolumeType");
+
+ if (volType == VolumeType::Percent)
+ usePercent->setChecked(true);
+
+ QHBoxLayout *volLayout = new QHBoxLayout();
+ volLayout->setContentsMargins(0, 0, 0, 0);
+ volLayout->addWidget(volLabel);
+ volLayout->addWidget(usePercent);
+ volLayout->addStretch();
+
int idx = 0;
mainLayout = new QGridLayout;
mainLayout->setContentsMargins(0, 0, 0, 0);
@@ -37,9 +59,7 @@ OBSBasicAdvAudio::OBSBasicAdvAudio(QWidget *parent)
label = new QLabel(QTStr("Basic.Stats.Status"));
label->setStyleSheet("font-weight: bold;");
mainLayout->addWidget(label, 0, idx++);
- label = new QLabel(QTStr("Basic.AdvAudio.Volume"));
- label->setStyleSheet("font-weight: bold;");
- mainLayout->addWidget(label, 0, idx++);
+ mainLayout->addLayout(volLayout, 0, idx++);
label = new QLabel(QTStr("Basic.AdvAudio.Mono"));
label->setStyleSheet("font-weight: bold;");
mainLayout->addWidget(label, 0, idx++);
@@ -193,10 +213,14 @@ void OBSBasicAdvAudio::SourceRemoved(OBSSource source)
}
}
-void OBSBasicAdvAudio::SetVolumeType()
+void OBSBasicAdvAudio::SetVolumeType(bool percent)
{
- QAction *action = reinterpret_cast(sender());
- VolumeType type = (VolumeType)action->property("volumeType").toInt();
+ VolumeType type;
+
+ if (percent)
+ type = VolumeType::Percent;
+ else
+ type = VolumeType::dB;
for (size_t i = 0; i < controls.size(); i++)
controls[i]->SetVolumeWidget(type);
@@ -205,38 +229,6 @@ void OBSBasicAdvAudio::SetVolumeType()
(int)type);
}
-void OBSBasicAdvAudio::ShowContextMenu(const QPoint &pos)
-{
- VolumeType type = (VolumeType)config_get_int(
- GetGlobalConfig(), "BasicWindow", "AdvAudioVolumeType");
-
- QMenu *contextMenu = new QMenu(this);
-
- QAction *percent = new QAction(QTStr("Percent"), this);
- QAction *dB = new QAction(QTStr("dB"), this);
-
- percent->setProperty("volumeType", (int)VolumeType::Percent);
- dB->setProperty("volumeType", (int)VolumeType::dB);
-
- connect(percent, SIGNAL(triggered()), this, SLOT(SetVolumeType()),
- Qt::DirectConnection);
- connect(dB, SIGNAL(triggered()), this, SLOT(SetVolumeType()),
- Qt::DirectConnection);
-
- percent->setCheckable(true);
- dB->setCheckable(true);
-
- if (type == VolumeType::Percent)
- percent->setChecked(true);
- else if (type == VolumeType::dB)
- dB->setChecked(true);
-
- contextMenu->addAction(dB);
- contextMenu->addAction(percent);
-
- contextMenu->exec(mapToGlobal(pos));
-}
-
void OBSBasicAdvAudio::ActiveOnlyChanged(bool checked)
{
SetShowInactive(!checked);
diff --git a/UI/window-basic-adv-audio.hpp b/UI/window-basic-adv-audio.hpp
index c982272..5d8508c 100644
--- a/UI/window-basic-adv-audio.hpp
+++ b/UI/window-basic-adv-audio.hpp
@@ -18,6 +18,7 @@ class OBSBasicAdvAudio : public QDialog {
QWidget *controlArea;
QGridLayout *mainLayout;
QPointer activeOnly;
+ QPointer usePercent;
OBSSignal sourceAddedSignal;
OBSSignal sourceRemovedSignal;
bool showInactive;
@@ -36,8 +37,7 @@ public slots:
void SourceAdded(OBSSource source);
void SourceRemoved(OBSSource source);
- void ShowContextMenu(const QPoint &pos);
- void SetVolumeType();
+ void SetVolumeType(bool percent);
void ActiveOnlyChanged(bool checked);
public:
diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp
index e62acd3..c41f1c1 100644
--- a/UI/window-basic-auto-config-test.cpp
+++ b/UI/window-basic-auto-config-test.cpp
@@ -4,6 +4,7 @@
#include
#include
+#include
#include
#include
#include
@@ -249,9 +250,10 @@ void AutoConfigTestPage::TestBandwidthThread()
GetServers(servers);
/* just use the first server if it only has one alternate server,
- * or if using Mixer or Restream due to their "auto" servers */
- if (servers.size() < 3 || wiz->serviceName == "Mixer.com - FTL" ||
- wiz->serviceName.substr(0, 11) == "Restream.io") {
+ * or if using Restream or Nimo TV due to their "auto" servers */
+ if (servers.size() < 3 ||
+ wiz->serviceName.substr(0, 11) == "Restream.io" ||
+ wiz->serviceName == "Nimo TV") {
servers.resize(1);
} else if (wiz->service == AutoConfig::Service::Twitch &&
@@ -413,12 +415,14 @@ void AutoConfigTestPage::TestBandwidthThread()
cv.wait(ul);
uint64_t total_time = os_gettime_ns() - t_start;
+ if (total_time == 0)
+ total_time = 1;
int total_bytes =
(int)obs_output_get_total_bytes(output) - start_bytes;
- uint64_t bitrate = (uint64_t)total_bytes * 8 * 1000000000 /
- total_time / 1000;
-
+ uint64_t bitrate = util_mul_div64(
+ total_bytes, 8ULL * 1000000000ULL / 1000ULL,
+ total_time);
if (obs_output_get_frames_dropped(output) ||
(int)bitrate < (wiz->startingBitrate * 75 / 100)) {
server.bitrate = (int)bitrate * 70 / 100;
@@ -454,8 +458,8 @@ void AutoConfigTestPage::TestBandwidthThread()
}
}
- wiz->server = bestServer;
- wiz->serverName = bestServerName;
+ wiz->server = std::move(bestServer);
+ wiz->serverName = std::move(bestServerName);
wiz->idealBitrate = bestBitrate;
QMetaObject::invokeMethod(this, "NextStage");
@@ -624,11 +628,13 @@ bool AutoConfigTestPage::TestSoftwareEncoding()
int i = 0;
int count = 1;
- auto testRes = [&](long double div, int fps_num, int fps_den,
- bool force) {
+ auto testRes = [&](int cy, int fps_num, int fps_den, bool force) {
int per = ++i * 100 / count;
QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, per));
+ if (cy > baseCY)
+ return true;
+
/* no need for more than 3 tests max */
if (results.size() >= 3)
return true;
@@ -640,8 +646,8 @@ bool AutoConfigTestPage::TestSoftwareEncoding()
long double fps = ((long double)fps_num / (long double)fps_den);
- int cx = int((long double)baseCX / div);
- int cy = int((long double)baseCY / div);
+ int cx = int(((long double)baseCX / (long double)baseCY) *
+ (long double)cy);
if (!force && wiz->type != AutoConfig::Type::Recording) {
int est = EstimateMinBitrate(cx, cy, fps_num, fps_den);
@@ -697,38 +703,50 @@ bool AutoConfigTestPage::TestSoftwareEncoding()
};
if (wiz->specificFPSNum && wiz->specificFPSDen) {
- count = 5;
- if (!testRes(1.0, 0, 0, false))
+ count = 7;
+ if (!testRes(2160, 0, 0, false))
+ return false;
+ if (!testRes(1440, 0, 0, false))
return false;
- if (!testRes(1.5, 0, 0, false))
+ if (!testRes(1080, 0, 0, false))
return false;
- if (!testRes(1.0 / 0.6, 0, 0, false))
+ if (!testRes(720, 0, 0, false))
return false;
- if (!testRes(2.0, 0, 0, false))
+ if (!testRes(480, 0, 0, false))
return false;
- if (!testRes(2.25, 0, 0, true))
+ if (!testRes(360, 0, 0, false))
+ return false;
+ if (!testRes(240, 0, 0, true))
return false;
} else {
- count = 10;
- if (!testRes(1.0, 60, 1, false))
+ count = 14;
+ if (!testRes(2160, 60, 1, false))
+ return false;
+ if (!testRes(2160, 30, 1, false))
+ return false;
+ if (!testRes(1440, 60, 1, false))
+ return false;
+ if (!testRes(1440, 30, 1, false))
return false;
- if (!testRes(1.0, 30, 1, false))
+ if (!testRes(1080, 60, 1, false))
return false;
- if (!testRes(1.5, 60, 1, false))
+ if (!testRes(1080, 30, 1, false))
return false;
- if (!testRes(1.5, 30, 1, false))
+ if (!testRes(720, 60, 1, false))
return false;
- if (!testRes(1.0 / 0.6, 60, 1, false))
+ if (!testRes(720, 30, 1, false))
return false;
- if (!testRes(1.0 / 0.6, 30, 1, false))
+ if (!testRes(480, 60, 1, false))
return false;
- if (!testRes(2.0, 60, 1, false))
+ if (!testRes(480, 30, 1, false))
return false;
- if (!testRes(2.0, 30, 1, false))
+ if (!testRes(360, 60, 1, false))
return false;
- if (!testRes(2.25, 60, 1, false))
+ if (!testRes(360, 30, 1, false))
return false;
- if (!testRes(2.25, 30, 1, true))
+ if (!testRes(240, 60, 1, false))
+ return false;
+ if (!testRes(240, 30, 1, true))
return false;
}
@@ -787,8 +805,10 @@ void AutoConfigTestPage::FindIdealHardwareResolution()
maxDataRate = 1280 * 720 * 30 + 1000;
}
- auto testRes = [&](long double div, int fps_num, int fps_den,
- bool force) {
+ auto testRes = [&](int cy, int fps_num, int fps_den, bool force) {
+ if (cy > baseCY)
+ return;
+
if (results.size() >= 3)
return;
@@ -799,8 +819,8 @@ void AutoConfigTestPage::FindIdealHardwareResolution()
long double fps = ((long double)fps_num / (long double)fps_den);
- int cx = int((long double)baseCX / div);
- int cy = int((long double)baseCY / div);
+ int cx = int(((long double)baseCX / (long double)baseCY) *
+ (long double)cy);
long double rate = (long double)cx * (long double)cy * fps;
if (!force && rate > maxDataRate)
@@ -825,22 +845,28 @@ void AutoConfigTestPage::FindIdealHardwareResolution()
};
if (wiz->specificFPSNum && wiz->specificFPSDen) {
- testRes(1.0, 0, 0, false);
- testRes(1.5, 0, 0, false);
- testRes(1.0 / 0.6, 0, 0, false);
- testRes(2.0, 0, 0, false);
- testRes(2.25, 0, 0, true);
+ testRes(2160, 0, 0, false);
+ testRes(1440, 0, 0, false);
+ testRes(1080, 0, 0, false);
+ testRes(720, 0, 0, false);
+ testRes(480, 0, 0, false);
+ testRes(360, 0, 0, false);
+ testRes(240, 0, 0, true);
} else {
- testRes(1.0, 60, 1, false);
- testRes(1.0, 30, 1, false);
- testRes(1.5, 60, 1, false);
- testRes(1.5, 30, 1, false);
- testRes(1.0 / 0.6, 60, 1, false);
- testRes(1.0 / 0.6, 30, 1, false);
- testRes(2.0, 60, 1, false);
- testRes(2.0, 30, 1, false);
- testRes(2.25, 60, 1, false);
- testRes(2.25, 30, 1, true);
+ testRes(2160, 60, 1, false);
+ testRes(2160, 30, 1, false);
+ testRes(1440, 60, 1, false);
+ testRes(1440, 30, 1, false);
+ testRes(1080, 60, 1, false);
+ testRes(1080, 30, 1, false);
+ testRes(720, 60, 1, false);
+ testRes(720, 30, 1, false);
+ testRes(480, 60, 1, false);
+ testRes(480, 30, 1, false);
+ testRes(360, 60, 1, false);
+ testRes(360, 30, 1, false);
+ testRes(240, 60, 1, false);
+ testRes(240, 30, 1, true);
}
int minArea = 960 * 540 + 1000;
@@ -965,7 +991,7 @@ void AutoConfigTestPage::FinalizeResults()
return new QLabel(QTStr(str), this);
};
- if (wiz->type != AutoConfig::Type::Recording) {
+ if (wiz->type == AutoConfig::Type::Streaming) {
const char *serverType = wiz->customServer ? "rtmp_custom"
: "rtmp_common";
@@ -1068,7 +1094,7 @@ void AutoConfigTestPage::NextStage()
started = true;
}
- if (wiz->type == AutoConfig::Type::Recording) {
+ if (wiz->type != AutoConfig::Type::Streaming) {
stage = Stage::StreamEncoder;
} else if (!wiz->bandwidthTest) {
stage = Stage::BandwidthTest;
@@ -1138,8 +1164,17 @@ AutoConfigTestPage::~AutoConfigTestPage()
void AutoConfigTestPage::initializePage()
{
+ if (wiz->type == AutoConfig::Type::VirtualCam) {
+ wiz->idealResolutionCX = wiz->baseResolutionCX;
+ wiz->idealResolutionCY = wiz->baseResolutionCY;
+ wiz->idealFPSNum = 30;
+ wiz->idealFPSDen = 1;
+ stage = Stage::Finished;
+ } else {
+ stage = Stage::Starting;
+ }
+
setSubTitle(QTStr(SUBTITLE_TESTING));
- stage = Stage::Starting;
softwareTested = false;
cancel = false;
DeleteLayout(results);
diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp
index 8e88d51..06c364f 100644
--- a/UI/window-basic-auto-config.cpp
+++ b/UI/window-basic-auto-config.cpp
@@ -69,6 +69,18 @@ AutoConfigStartPage::AutoConfigStartPage(QWidget *parent)
ui->setupUi(this);
setTitle(QTStr("Basic.AutoConfig.StartPage"));
setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle"));
+
+ OBSBasic *main = OBSBasic::Get();
+ if (main->VCamEnabled()) {
+ QRadioButton *prioritizeVCam = new QRadioButton(
+ QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"),
+ this);
+ QBoxLayout *box = reinterpret_cast(layout());
+ box->insertWidget(2, prioritizeVCam);
+
+ connect(prioritizeVCam, &QPushButton::clicked, this,
+ &AutoConfigStartPage::PrioritizeVCam);
+ }
}
AutoConfigStartPage::~AutoConfigStartPage()
@@ -78,7 +90,9 @@ AutoConfigStartPage::~AutoConfigStartPage()
int AutoConfigStartPage::nextId() const
{
- return AutoConfig::VideoPage;
+ return wiz->type == AutoConfig::Type::VirtualCam
+ ? AutoConfig::TestPage
+ : AutoConfig::VideoPage;
}
void AutoConfigStartPage::on_prioritizeStreaming_clicked()
@@ -91,6 +105,11 @@ void AutoConfigStartPage::on_prioritizeRecording_clicked()
wiz->type = AutoConfig::Type::Recording;
}
+void AutoConfigStartPage::PrioritizeVCam()
+{
+ wiz->type = AutoConfig::Type::VirtualCam;
+}
+
/* ------------------------------------------------------------------------- */
#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x
@@ -255,6 +274,10 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent)
SLOT(ServiceChanged()));
connect(ui->customServer, SIGNAL(textChanged(const QString &)), this,
SLOT(ServiceChanged()));
+ connect(ui->customServer, SIGNAL(textChanged(const QString &)), this,
+ SLOT(UpdateKeyLink()));
+ connect(ui->customServer, SIGNAL(editingFinished()), this,
+ SLOT(UpdateKeyLink()));
connect(ui->doBandwidthTest, SIGNAL(toggled(bool)), this,
SLOT(ServiceChanged()));
@@ -557,12 +580,8 @@ void AutoConfigStreamPage::ServiceChanged()
void AutoConfigStreamPage::UpdateKeyLink()
{
- if (IsCustomService()) {
- ui->doBandwidthTest->setEnabled(true);
- return;
- }
-
QString serviceName = ui->service->currentText();
+ QString customServer = ui->customServer->text();
bool isYoutube = false;
QString streamKeyLink;
@@ -575,12 +594,16 @@ void AutoConfigStreamPage::UpdateKeyLink()
} else if (serviceName.startsWith("Restream.io")) {
streamKeyLink =
"https://restream.io/settings/streaming-setup?from=OBS";
- } else if (serviceName == "Facebook Live") {
- streamKeyLink = "https://www.facebook.com/live/create?ref=OBS";
+ } else if (serviceName == "Facebook Live" ||
+ (customServer.contains("fbcdn.net") && IsCustomService())) {
+ streamKeyLink =
+ "https://www.facebook.com/live/producer?ref=OBS";
} else if (serviceName.startsWith("Twitter")) {
streamKeyLink = "https://www.pscp.tv/account/producer";
} else if (serviceName.startsWith("YouStreamer")) {
streamKeyLink = "https://www.app.youstreamer.com/stream/";
+ } else if (serviceName == "Trovo") {
+ streamKeyLink = "https://studio.trovo.live/mychannel/stream";
}
if (QString(streamKeyLink).isNull()) {
@@ -623,7 +646,7 @@ void AutoConfigStreamPage::LoadServices(bool showAll)
}
if (showAll)
- names.sort();
+ names.sort(Qt::CaseInsensitive);
for (QString &name : names)
ui->service->addItem(name);
@@ -726,7 +749,7 @@ AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent)
std::string serviceType;
GetServiceInfo(serviceType, serviceName, server, key);
-#ifdef _WIN32
+#if defined(_WIN32) || defined(__APPLE__)
setWizardStyle(QWizard::ModernStyle);
#endif
streamPage = new AutoConfigStreamPage();
@@ -831,7 +854,7 @@ AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent)
streamPage->ui->preferHardware->setChecked(preferHardware);
}
- setOptions(0);
+ setOptions(QWizard::WizardOptions());
setButtonText(QWizard::FinishButton,
QTStr("Basic.AutoConfig.ApplySettings"));
setButtonText(QWizard::BackButton, QTStr("Back"));
diff --git a/UI/window-basic-auto-config.hpp b/UI/window-basic-auto-config.hpp
index 874bacb..98540c8 100644
--- a/UI/window-basic-auto-config.hpp
+++ b/UI/window-basic-auto-config.hpp
@@ -33,6 +33,7 @@ class AutoConfig : public QWizard {
Invalid,
Streaming,
Recording,
+ VirtualCam,
};
enum class Service {
@@ -139,6 +140,7 @@ class AutoConfigStartPage : public QWizardPage {
public slots:
void on_prioritizeStreaming_clicked();
void on_prioritizeRecording_clicked();
+ void PrioritizeVCam();
};
class AutoConfigVideoPage : public QWizardPage {
diff --git a/UI/window-basic-filters.cpp b/UI/window-basic-filters.cpp
index 2b7d7b4..bffa3d8 100644
--- a/UI/window-basic-filters.cpp
+++ b/UI/window-basic-filters.cpp
@@ -402,6 +402,8 @@ QMenu *OBSBasicFilters::CreateAddFilterPopupMenu(bool async)
continue;
if ((caps & OBS_SOURCE_CAP_DISABLED) != 0)
continue;
+ if ((caps & OBS_SOURCE_CAP_OBSOLETE) != 0)
+ continue;
auto it = types.begin();
for (; it != types.end(); ++it) {
diff --git a/UI/window-basic-interaction.cpp b/UI/window-basic-interaction.cpp
index 4e702b9..333a263 100644
--- a/UI/window-basic-interaction.cpp
+++ b/UI/window-basic-interaction.cpp
@@ -314,22 +314,32 @@ bool OBSBasicInteraction::HandleMouseWheelEvent(QWheelEvent *event)
int xDelta = 0;
int yDelta = 0;
+ const QPoint angleDelta = event->angleDelta();
if (!event->pixelDelta().isNull()) {
- if (event->orientation() == Qt::Horizontal)
+ if (angleDelta.x())
xDelta = event->pixelDelta().x();
else
yDelta = event->pixelDelta().y();
} else {
- if (event->orientation() == Qt::Horizontal)
- xDelta = event->delta();
+ if (angleDelta.x())
+ xDelta = angleDelta.x();
else
- yDelta = event->delta();
+ yDelta = angleDelta.y();
}
- if (GetSourceRelativeXY(event->x(), event->y(), mouseEvent.x,
- mouseEvent.y))
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ const QPointF position = event->position();
+ const int x = position.x();
+ const int y = position.y();
+#else
+ const int x = event->x();
+ const int y = event->y();
+#endif
+
+ if (GetSourceRelativeXY(x, y, mouseEvent.x, mouseEvent.y)) {
obs_source_send_mouse_wheel(source, &mouseEvent, xDelta,
yDelta);
+ }
return true;
}
diff --git a/UI/window-basic-interaction.hpp b/UI/window-basic-interaction.hpp
index 1788deb..4d53b83 100644
--- a/UI/window-basic-interaction.hpp
+++ b/UI/window-basic-interaction.hpp
@@ -80,6 +80,6 @@ class OBSEventFilter : public QObject {
return filter(obj, event);
}
-private:
+public:
EventFilterFunc filter;
};
diff --git a/UI/window-basic-main-dropfiles.cpp b/UI/window-basic-main-dropfiles.cpp
index 462161b..a10262e 100644
--- a/UI/window-basic-main-dropfiles.cpp
+++ b/UI/window-basic-main-dropfiles.cpp
@@ -51,8 +51,11 @@ static string GenerateSourceName(const char *base)
}
obs_source_t *source = obs_get_source_by_name(name.c_str());
+
if (!source)
return name;
+ else
+ obs_source_release(source);
}
}
diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp
index f68093c..6892db1 100644
--- a/UI/window-basic-main-outputs.cpp
+++ b/UI/window-basic-main-outputs.cpp
@@ -14,6 +14,7 @@ volatile bool streaming_active = false;
volatile bool recording_active = false;
volatile bool recording_paused = false;
volatile bool replaybuf_active = false;
+volatile bool virtualcam_active = false;
#define RTMP_PROTOCOL "rtmp"
@@ -139,34 +140,28 @@ static void OBSReplayBufferStopping(void *data, calldata_t *params)
UNUSED_PARAMETER(params);
}
-static void FindBestFilename(string &strPath, bool noSpace)
+static void OBSStartVirtualCam(void *data, calldata_t *params)
{
- int num = 2;
-
- if (!os_file_exists(strPath.c_str()))
- return;
+ BasicOutputHandler *output = static_cast(data);
- const char *ext = strrchr(strPath.c_str(), '.');
- if (!ext)
- return;
+ output->virtualCamActive = true;
+ os_atomic_set_bool(&virtualcam_active, true);
+ QMetaObject::invokeMethod(output->main, "OnVirtualCamStart");
- int extStart = int(ext - strPath.c_str());
- for (;;) {
- string testPath = strPath;
- string numStr;
+ UNUSED_PARAMETER(params);
+}
- numStr = noSpace ? "_" : " (";
- numStr += to_string(num++);
- if (!noSpace)
- numStr += ")";
+static void OBSStopVirtualCam(void *data, calldata_t *params)
+{
+ BasicOutputHandler *output = static_cast(data);
+ int code = (int)calldata_int(params, "code");
- testPath.insert(extStart, numStr);
+ output->virtualCamActive = false;
+ os_atomic_set_bool(&virtualcam_active, false);
+ QMetaObject::invokeMethod(output->main, "OnVirtualCamStop",
+ Q_ARG(int, code));
- if (!os_file_exists(testPath.c_str())) {
- strPath = testPath;
- break;
- }
- }
+ UNUSED_PARAMETER(params);
}
/* ------------------------------------------------------------------------ */
@@ -197,6 +192,52 @@ static bool CreateAACEncoder(OBSEncoder &res, string &id, int bitrate,
/* ------------------------------------------------------------------------ */
+inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_)
+{
+ if (main->vcamEnabled) {
+ virtualCam = obs_output_create("virtualcam_output",
+ "virtualcam_output", nullptr,
+ nullptr);
+ obs_output_release(virtualCam);
+
+ signal_handler_t *signal =
+ obs_output_get_signal_handler(virtualCam);
+ startVirtualCam.Connect(signal, "start", OBSStartVirtualCam,
+ this);
+ stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this);
+ }
+}
+
+bool BasicOutputHandler::StartVirtualCam()
+{
+ if (main->vcamEnabled) {
+ obs_output_set_media(virtualCam, obs_get_video(),
+ obs_get_audio());
+ if (!Active())
+ SetupOutputs();
+
+ return obs_output_start(virtualCam);
+ }
+ return false;
+}
+
+void BasicOutputHandler::StopVirtualCam()
+{
+ if (main->vcamEnabled) {
+ obs_output_stop(virtualCam);
+ }
+}
+
+bool BasicOutputHandler::VirtualCamActive() const
+{
+ if (main->vcamEnabled) {
+ return obs_output_active(virtualCam);
+ }
+ return false;
+}
+
+/* ------------------------------------------------------------------------ */
+
struct SimpleOutput : BasicOutputHandler {
OBSEncoder aacStreaming;
OBSEncoder h264Streaming;
@@ -226,7 +267,7 @@ struct SimpleOutput : BasicOutputHandler {
void UpdateRecordingAudioSettings();
virtual void Update() override;
- void SetupOutputs();
+ void SetupOutputs() override;
int GetAudioBitrate() const;
void LoadRecordingPreset_h264(const char *encoder);
@@ -238,6 +279,7 @@ struct SimpleOutput : BasicOutputHandler {
void UpdateRecording();
bool ConfigureRecording(bool useReplayBuffer);
+ virtual bool SetupStreaming(obs_service_t *service) override;
virtual bool StartStreaming(obs_service_t *service) override;
virtual bool StartRecording() override;
virtual bool StartReplayBuffer() override;
@@ -367,9 +409,14 @@ SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_)
bool useReplayBuffer = config_get_bool(main->Config(),
"SimpleOutput", "RecRB");
if (useReplayBuffer) {
+ obs_data_t *hotkey;
const char *str = config_get_string(
main->Config(), "Hotkeys", "ReplayBuffer");
- obs_data_t *hotkey = obs_data_create_from_json(str);
+ if (str)
+ hotkey = obs_data_create_from_json(str);
+ else
+ hotkey = nullptr;
+
replayBuffer = obs_output_create("replay_buffer",
Str("ReplayBuffer"),
nullptr, hotkey);
@@ -680,7 +727,7 @@ const char *FindAudioEncoderFromCodec(const char *type)
return nullptr;
}
-bool SimpleOutput::StartStreaming(obs_service_t *service)
+bool SimpleOutput::SetupStreaming(obs_service_t *service)
{
if (!Active())
SetupOutputs();
@@ -697,7 +744,7 @@ bool SimpleOutput::StartStreaming(obs_service_t *service)
const char *url = obs_service_get_url(service);
if (url != NULL &&
strncmp(url, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) != 0) {
- type = "ffmpeg_encoded_output";
+ type = "ffmpeg_mpegts_muxer";
}
}
@@ -774,9 +821,11 @@ bool SimpleOutput::StartStreaming(obs_service_t *service)
obs_output_set_video_encoder(streamOutput, h264Streaming);
obs_output_set_audio_encoder(streamOutput, aacStreaming, 0);
obs_output_set_service(streamOutput, service);
+ return true;
+}
- /* --------------------- */
-
+bool SimpleOutput::StartStreaming(obs_service_t *service)
+{
bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect");
int retryDelay =
config_get_uint(main->Config(), "Output", "RetryDelay");
@@ -825,36 +874,12 @@ bool SimpleOutput::StartStreaming(obs_service_t *service)
else
lastError = string();
+ const char *type = obs_service_get_output_type(service);
blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type,
hasLastError ? " Last Error: " : "", hasLastError ? error : "");
return false;
}
-static void remove_reserved_file_characters(string &s)
-{
- replace(s.begin(), s.end(), '/', '_');
- replace(s.begin(), s.end(), '\\', '_');
- replace(s.begin(), s.end(), '*', '_');
- replace(s.begin(), s.end(), '?', '_');
- replace(s.begin(), s.end(), '"', '_');
- replace(s.begin(), s.end(), '|', '_');
- replace(s.begin(), s.end(), ':', '_');
- replace(s.begin(), s.end(), '>', '_');
- replace(s.begin(), s.end(), '<', '_');
-}
-
-static void ensure_directory_exists(string &path)
-{
- replace(path.begin(), path.end(), '\\', '/');
-
- size_t last = path.rfind('/');
- if (last == string::npos)
- return;
-
- string directory = path.substr(0, last);
- os_mkdirs(directory.c_str());
-}
-
void SimpleOutput::UpdateRecording()
{
if (replayBufferActive || recordingActive)
@@ -905,54 +930,15 @@ bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer)
int rbSize =
config_get_int(main->Config(), "SimpleOutput", "RecRBSize");
- os_dir_t *dir = path && path[0] ? os_opendir(path) : nullptr;
-
- if (!dir) {
- if (main->isVisible())
- OBSMessageBox::warning(main,
- QTStr("Output.BadPath.Title"),
- QTStr("Output.BadPath.Text"));
- else
- main->SysTrayNotify(QTStr("Output.BadPath.Text"),
- QSystemTrayIcon::Warning);
- return false;
- }
-
- os_closedir(dir);
-
+ string f;
string strPath;
- strPath += path;
-
- char lastChar = strPath.back();
- if (lastChar != '/' && lastChar != '\\')
- strPath += "/";
-
- strPath += GenerateSpecifiedFilename(ffmpegOutput ? "avi" : format,
- noSpace, filenameFormat);
- ensure_directory_exists(strPath);
- if (!overwriteIfExists)
- FindBestFilename(strPath, noSpace);
obs_data_t *settings = obs_data_create();
if (updateReplayBuffer) {
- string f;
-
- if (rbPrefix && *rbPrefix) {
- f += rbPrefix;
- if (f.back() != ' ')
- f += " ";
- }
-
- f += filenameFormat;
-
- if (rbSuffix && *rbSuffix) {
- if (*rbSuffix != ' ')
- f += " ";
- f += rbSuffix;
- }
-
- remove_reserved_file_characters(f);
-
+ f = GetFormatString(filenameFormat, rbPrefix, rbSuffix);
+ strPath = GetOutputFilename(path, ffmpegOutput ? "avi" : format,
+ noSpace, overwriteIfExists,
+ f.c_str());
obs_data_set_string(settings, "directory", path);
obs_data_set_string(settings, "format", f.c_str());
obs_data_set_string(settings, "extension", format);
@@ -961,6 +947,11 @@ bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer)
obs_data_set_int(settings, "max_size_mb",
usingRecordingPreset ? rbSize : 0);
} else {
+ f = GetFormatString(filenameFormat, nullptr, nullptr);
+ strPath = GetRecordingFilename(path,
+ ffmpegOutput ? "avi" : format,
+ noSpace, overwriteIfExists,
+ f.c_str(), ffmpegOutput);
obs_data_set_string(settings, ffmpegOutput ? "url" : "path",
strPath.c_str());
}
@@ -1075,9 +1066,10 @@ struct AdvancedOutput : BasicOutputHandler {
inline void SetupStreaming();
inline void SetupRecording();
inline void SetupFFmpeg();
- void SetupOutputs();
+ void SetupOutputs() override;
int GetAudioBitrate(size_t i) const;
+ virtual bool SetupStreaming(obs_service_t *service) override;
virtual bool StartStreaming(obs_service_t *service) override;
virtual bool StartRecording() override;
virtual bool StartReplayBuffer() override;
@@ -1512,7 +1504,7 @@ int AdvancedOutput::GetAudioBitrate(size_t i) const
return FindClosestAvailableAACBitrate(bitrate);
}
-bool AdvancedOutput::StartStreaming(obs_service_t *service)
+bool AdvancedOutput::SetupStreaming(obs_service_t *service)
{
int streamTrack =
config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1;
@@ -1539,7 +1531,7 @@ bool AdvancedOutput::StartStreaming(obs_service_t *service)
const char *url = obs_service_get_url(service);
if (url != NULL &&
strncmp(url, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) != 0) {
- type = "ffmpeg_encoded_output";
+ type = "ffmpeg_mpegts_muxer";
}
}
@@ -1614,9 +1606,11 @@ bool AdvancedOutput::StartStreaming(obs_service_t *service)
obs_output_set_video_encoder(streamOutput, h264Streaming);
obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0);
+ return true;
+}
- /* --------------------- */
-
+bool AdvancedOutput::StartStreaming(obs_service_t *service)
+{
obs_output_set_service(streamOutput, service);
bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect");
@@ -1665,6 +1659,7 @@ bool AdvancedOutput::StartStreaming(obs_service_t *service)
else
lastError = string();
+ const char *type = obs_service_get_output_type(service);
blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type,
hasLastError ? " Last Error: " : "", hasLastError ? error : "");
return false;
@@ -1707,34 +1702,10 @@ bool AdvancedOutput::StartRecording()
? "FFFileNameWithoutSpace"
: "RecFileNameWithoutSpace");
- os_dir_t *dir = path && path[0] ? os_opendir(path) : nullptr;
-
- if (!dir) {
- if (main->isVisible())
- OBSMessageBox::warning(
- main, QTStr("Output.BadPath.Title"),
- QTStr("Output.BadPath.Text"));
- else
- main->SysTrayNotify(
- QTStr("Output.BadPath.Text"),
- QSystemTrayIcon::Warning);
- return false;
- }
-
- os_closedir(dir);
-
- string strPath;
- strPath += path;
-
- char lastChar = strPath.back();
- if (lastChar != '/' && lastChar != '\\')
- strPath += "/";
-
- strPath += GenerateSpecifiedFilename(recFormat, noSpace,
- filenameFormat);
- ensure_directory_exists(strPath);
- if (!overwriteIfExists)
- FindBestFilename(strPath, noSpace);
+ string strPath = GetRecordingFilename(path, recFormat, noSpace,
+ overwriteIfExists,
+ filenameFormat,
+ ffmpegRecording);
obs_data_t *settings = obs_data_create();
obs_data_set_string(settings, ffmpegRecording ? "url" : "path",
@@ -1807,53 +1778,11 @@ bool AdvancedOutput::StartReplayBuffer()
rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime");
rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize");
- os_dir_t *dir = path && path[0] ? os_opendir(path) : nullptr;
-
- if (!dir) {
- if (main->isVisible())
- OBSMessageBox::warning(
- main, QTStr("Output.BadPath.Title"),
- QTStr("Output.BadPath.Text"));
- else
- main->SysTrayNotify(
- QTStr("Output.BadPath.Text"),
- QSystemTrayIcon::Warning);
- return false;
- }
-
- os_closedir(dir);
-
- string strPath;
- strPath += path;
-
- char lastChar = strPath.back();
- if (lastChar != '/' && lastChar != '\\')
- strPath += "/";
-
- strPath += GenerateSpecifiedFilename(recFormat, noSpace,
- filenameFormat);
- ensure_directory_exists(strPath);
- if (!overwriteIfExists)
- FindBestFilename(strPath, noSpace);
+ string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix);
+ string strPath = GetOutputFilename(
+ path, recFormat, noSpace, overwriteIfExists, f.c_str());
obs_data_t *settings = obs_data_create();
- string f;
-
- if (rbPrefix && *rbPrefix) {
- f += rbPrefix;
- if (f.back() != ' ')
- f += " ";
- }
-
- f += filenameFormat;
-
- if (rbSuffix && *rbSuffix) {
- if (*rbSuffix != ' ')
- f += " ";
- f += rbSuffix;
- }
-
- remove_reserved_file_characters(f);
obs_data_set_string(settings, "directory", path);
obs_data_set_string(settings, "format", f.c_str());
@@ -1919,6 +1848,25 @@ bool AdvancedOutput::ReplayBufferActive() const
/* ------------------------------------------------------------------------ */
+bool BasicOutputHandler::SetupAutoRemux(const char *&ext)
+{
+ bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux");
+ if (autoRemux && strcmp(ext, "mp4") == 0)
+ ext = "mkv";
+ return autoRemux;
+}
+
+std::string
+BasicOutputHandler::GetRecordingFilename(const char *path, const char *ext,
+ bool noSpace, bool overwrite,
+ const char *format, bool ffmpeg)
+{
+ bool remux = !ffmpeg && SetupAutoRemux(ext);
+ string dst = GetOutputFilename(path, ext, noSpace, overwrite, format);
+ lastRecordingPath = remux ? dst : "";
+ return dst;
+}
+
BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main)
{
return new SimpleOutput(main);
diff --git a/UI/window-basic-main-outputs.hpp b/UI/window-basic-main-outputs.hpp
index b5aae77..5f9f0bd 100644
--- a/UI/window-basic-main-outputs.hpp
+++ b/UI/window-basic-main-outputs.hpp
@@ -8,47 +8,64 @@ struct BasicOutputHandler {
OBSOutput fileOutput;
OBSOutput streamOutput;
OBSOutput replayBuffer;
+ OBSOutput virtualCam;
bool streamingActive = false;
bool recordingActive = false;
bool delayActive = false;
bool replayBufferActive = false;
+ bool virtualCamActive = false;
OBSBasic *main;
std::string outputType;
std::string lastError;
+ std::string lastRecordingPath;
+
OBSSignal startRecording;
OBSSignal stopRecording;
OBSSignal startReplayBuffer;
OBSSignal stopReplayBuffer;
OBSSignal startStreaming;
OBSSignal stopStreaming;
+ OBSSignal startVirtualCam;
+ OBSSignal stopVirtualCam;
OBSSignal streamDelayStarting;
OBSSignal streamStopping;
OBSSignal recordStopping;
OBSSignal replayBufferStopping;
- inline BasicOutputHandler(OBSBasic *main_) : main(main_) {}
+ inline BasicOutputHandler(OBSBasic *main_);
virtual ~BasicOutputHandler(){};
+ virtual bool SetupStreaming(obs_service_t *service) = 0;
virtual bool StartStreaming(obs_service_t *service) = 0;
virtual bool StartRecording() = 0;
virtual bool StartReplayBuffer() { return false; }
+ virtual bool StartVirtualCam();
virtual void StopStreaming(bool force = false) = 0;
virtual void StopRecording(bool force = false) = 0;
virtual void StopReplayBuffer(bool force = false) { (void)force; }
+ virtual void StopVirtualCam();
virtual bool StreamingActive() const = 0;
virtual bool RecordingActive() const = 0;
virtual bool ReplayBufferActive() const { return false; }
+ virtual bool VirtualCamActive() const;
virtual void Update() = 0;
+ virtual void SetupOutputs() = 0;
inline bool Active() const
{
return streamingActive || recordingActive || delayActive ||
- replayBufferActive;
+ replayBufferActive || virtualCamActive;
}
+
+protected:
+ bool SetupAutoRemux(const char *&ext);
+ std::string GetRecordingFilename(const char *path, const char *ext,
+ bool noSpace, bool overwrite,
+ const char *format, bool ffmpeg);
};
BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main);
diff --git a/UI/window-basic-main-profiles.cpp b/UI/window-basic-main-profiles.cpp
index bf7bd11..595314a 100644
--- a/UI/window-basic-main-profiles.cpp
+++ b/UI/window-basic-main-profiles.cpp
@@ -496,9 +496,8 @@ void OBSBasic::on_actionImportProfile_triggered()
return;
}
- QString dir = QFileDialog::getExistingDirectory(
- this, QTStr("Basic.MainMenu.Profile.Import"), home,
- QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
+ QString dir = SelectDirectory(
+ this, QTStr("Basic.MainMenu.Profile.Import"), home);
if (!dir.isEmpty() && !dir.isNull()) {
QString inputPath = QString::fromUtf8(path);
@@ -543,9 +542,8 @@ void OBSBasic::on_actionExportProfile_triggered()
return;
}
- QString dir = QFileDialog::getExistingDirectory(
- this, QTStr("Basic.MainMenu.Profile.Export"), home,
- QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
+ QString dir = SelectDirectory(
+ this, QTStr("Basic.MainMenu.Profile.Export"), home);
if (!dir.isEmpty() && !dir.isNull()) {
QString outputDir = dir + "/" + currentProfile;
diff --git a/UI/window-basic-main-scene-collections.cpp b/UI/window-basic-main-scene-collections.cpp
index 6f61194..b45e5e0 100644
--- a/UI/window-basic-main-scene-collections.cpp
+++ b/UI/window-basic-main-scene-collections.cpp
@@ -92,7 +92,6 @@ static bool SceneCollectionExists(const char *findName)
static bool GetUnusedSceneCollectionFile(std::string &name, std::string &file)
{
char path[512];
- size_t len;
int ret;
if (!GetFileSafeName(name.c_str(), file)) {
@@ -107,7 +106,6 @@ static bool GetUnusedSceneCollectionFile(std::string &name, std::string &file)
return false;
}
- len = file.size();
file.insert(0, path);
if (!GetClosestUnusedFileName(file, "json")) {
@@ -117,7 +115,7 @@ static bool GetUnusedSceneCollectionFile(std::string &name, std::string &file)
}
file.erase(file.size() - 5, 5);
- file.erase(0, file.size() - len);
+ file.erase(0, strlen(path));
return true;
}
@@ -414,9 +412,9 @@ void OBSBasic::on_actionExportSceneCollection_triggered()
return;
}
- QString exportFile = QFileDialog::getSaveFileName(
- this, QTStr("Basic.MainMenu.SceneCollection.Export"),
- home + "/" + currentFile, "JSON Files (*.json)");
+ QString exportFile =
+ SaveFile(this, QTStr("Basic.MainMenu.SceneCollection.Export"),
+ home + "/" + currentFile, "JSON Files (*.json)");
string file = QT_TO_UTF8(exportFile);
diff --git a/UI/window-basic-main-screenshot.cpp b/UI/window-basic-main-screenshot.cpp
new file mode 100644
index 0000000..a803f67
--- /dev/null
+++ b/UI/window-basic-main-screenshot.cpp
@@ -0,0 +1,215 @@
+/******************************************************************************
+ Copyright (C) 2020 by Hugh Bailey
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+******************************************************************************/
+
+#include "window-basic-main.hpp"
+#include "screenshot-obj.hpp"
+#include "qt-wrappers.hpp"
+
+static void ScreenshotTick(void *param, float);
+
+/* ========================================================================= */
+
+ScreenshotObj::ScreenshotObj(obs_source_t *source)
+ : weakSource(OBSGetWeakRef(source))
+{
+ obs_add_tick_callback(ScreenshotTick, this);
+}
+
+ScreenshotObj::~ScreenshotObj()
+{
+ obs_enter_graphics();
+ gs_stagesurface_destroy(stagesurf);
+ gs_texrender_destroy(texrender);
+ obs_leave_graphics();
+
+ obs_remove_tick_callback(ScreenshotTick, this);
+ if (th.joinable())
+ th.join();
+}
+
+void ScreenshotObj::Screenshot()
+{
+ OBSSource source = OBSGetStrongRef(weakSource);
+
+ if (source) {
+ cx = obs_source_get_base_width(source);
+ cy = obs_source_get_base_height(source);
+ } else {
+ obs_video_info ovi;
+ obs_get_video_info(&ovi);
+ cx = ovi.base_width;
+ cy = ovi.base_height;
+ }
+
+ if (!cx || !cy) {
+ blog(LOG_WARNING, "Cannot screenshot, invalid target size");
+ obs_remove_tick_callback(ScreenshotTick, this);
+ deleteLater();
+ return;
+ }
+
+ texrender = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
+ stagesurf = gs_stagesurface_create(cx, cy, GS_RGBA);
+
+ gs_texrender_reset(texrender);
+ if (gs_texrender_begin(texrender, cx, cy)) {
+ vec4 zero;
+ vec4_zero(&zero);
+
+ gs_clear(GS_CLEAR_COLOR, &zero, 0.0f, 0);
+ gs_ortho(0.0f, (float)cx, 0.0f, (float)cy, -100.0f, 100.0f);
+
+ gs_blend_state_push();
+ gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
+
+ if (source) {
+ obs_source_inc_showing(source);
+ obs_source_video_render(source);
+ obs_source_dec_showing(source);
+ } else {
+ obs_render_main_texture();
+ }
+
+ gs_blend_state_pop();
+ gs_texrender_end(texrender);
+ }
+}
+
+void ScreenshotObj::Download()
+{
+ gs_stage_texture(stagesurf, gs_texrender_get_texture(texrender));
+}
+
+void ScreenshotObj::Copy()
+{
+ uint8_t *videoData = nullptr;
+ uint32_t videoLinesize = 0;
+
+ image = QImage(cx, cy, QImage::Format::Format_RGBX8888);
+
+ if (gs_stagesurface_map(stagesurf, &videoData, &videoLinesize)) {
+ int linesize = image.bytesPerLine();
+ for (int y = 0; y < (int)cy; y++)
+ memcpy(image.scanLine(y),
+ videoData + (y * videoLinesize), linesize);
+
+ gs_stagesurface_unmap(stagesurf);
+ }
+}
+
+void ScreenshotObj::Save()
+{
+ OBSBasic *main = OBSBasic::Get();
+ config_t *config = main->Config();
+
+ const char *mode = config_get_string(config, "Output", "Mode");
+ const char *type = config_get_string(config, "AdvOut", "RecType");
+ const char *adv_path =
+ strcmp(type, "Standard")
+ ? config_get_string(config, "AdvOut", "FFFilePath")
+ : config_get_string(config, "AdvOut", "RecFilePath");
+ const char *rec_path =
+ strcmp(mode, "Advanced")
+ ? config_get_string(config, "SimpleOutput", "FilePath")
+ : adv_path;
+
+ const char *filenameFormat =
+ config_get_string(config, "Output", "FilenameFormatting");
+ bool overwriteIfExists =
+ config_get_bool(config, "Output", "OverwriteIfExists");
+
+ path = GetOutputFilename(
+ rec_path, "png", false, overwriteIfExists,
+ GetFormatString(filenameFormat, "Screenshot", nullptr).c_str());
+
+ th = std::thread([this] { MuxAndFinish(); });
+}
+
+void ScreenshotObj::MuxAndFinish()
+{
+ image.save(QT_UTF8(path.c_str()));
+ blog(LOG_INFO, "Saved screenshot to '%s'", path.c_str());
+ deleteLater();
+}
+
+/* ========================================================================= */
+
+#define STAGE_SCREENSHOT 0
+#define STAGE_DOWNLOAD 1
+#define STAGE_COPY_AND_SAVE 2
+#define STAGE_FINISH 3
+
+static void ScreenshotTick(void *param, float)
+{
+ ScreenshotObj *data = reinterpret_cast(param);
+
+ if (data->stage == STAGE_FINISH) {
+ return;
+ }
+
+ obs_enter_graphics();
+
+ switch (data->stage) {
+ case STAGE_SCREENSHOT:
+ data->Screenshot();
+ break;
+ case STAGE_DOWNLOAD:
+ data->Download();
+ break;
+ case STAGE_COPY_AND_SAVE:
+ data->Copy();
+ QMetaObject::invokeMethod(data, "Save");
+ obs_remove_tick_callback(ScreenshotTick, data);
+ break;
+ }
+
+ obs_leave_graphics();
+
+ data->stage++;
+}
+
+void OBSBasic::Screenshot(OBSSource source)
+{
+ if (!!screenshotData) {
+ blog(LOG_WARNING, "Cannot take new screenshot, "
+ "screenshot currently in progress");
+ return;
+ }
+
+ screenshotData = new ScreenshotObj(source);
+}
+
+void OBSBasic::ScreenshotSelectedSource()
+{
+ OBSSceneItem item = GetCurrentSceneItem();
+ if (item) {
+ Screenshot(obs_sceneitem_get_source(item));
+ } else {
+ blog(LOG_INFO, "Could not take a source screenshot: "
+ "no source selected");
+ }
+}
+
+void OBSBasic::ScreenshotProgram()
+{
+ Screenshot(GetProgramSource());
+}
+
+void OBSBasic::ScreenshotScene()
+{
+ Screenshot(GetCurrentSceneSource());
+}
diff --git a/UI/window-basic-main-transitions.cpp b/UI/window-basic-main-transitions.cpp
index ea2d9a8..98b43cd 100644
--- a/UI/window-basic-main-transitions.cpp
+++ b/UI/window-basic-main-transitions.cpp
@@ -50,16 +50,23 @@ static inline QString MakeQuickTransitionText(QuickTransition *qt)
void OBSBasic::InitDefaultTransitions()
{
+ struct AddTransitionVal {
+ QString id;
+ QString name;
+ };
+
+ ui->transitions->blockSignals(true);
std::vector transitions;
+ std::vector addables;
size_t idx = 0;
const char *id;
/* automatically add transitions that have no configuration (things
* such as cut/fade/etc) */
while (obs_enum_transition_types(idx++, &id)) {
- if (!obs_is_source_configurable(id)) {
- const char *name = obs_source_get_display_name(id);
+ const char *name = obs_source_get_display_name(id);
+ if (!obs_is_source_configurable(id)) {
obs_source_t *tr =
obs_source_create_private(id, name, NULL);
InitTransition(tr);
@@ -67,8 +74,16 @@ void OBSBasic::InitDefaultTransitions()
if (strcmp(id, "fade_transition") == 0)
fadeTransition = tr;
+ else if (strcmp(id, "cut_transition") == 0)
+ cutTransition = tr;
obs_source_release(tr);
+ } else {
+ AddTransitionVal val;
+ val.name = QTStr("Add") + QStringLiteral(": ") +
+ QT_UTF8(name);
+ val.id = QT_UTF8(id);
+ addables.push_back(val);
}
}
@@ -76,6 +91,43 @@ void OBSBasic::InitDefaultTransitions()
ui->transitions->addItem(QT_UTF8(obs_source_get_name(tr)),
QVariant::fromValue(OBSSource(tr)));
}
+
+ if (addables.size())
+ ui->transitions->insertSeparator(ui->transitions->count());
+
+ for (AddTransitionVal &val : addables) {
+ ui->transitions->addItem(val.name, QVariant::fromValue(val.id));
+ }
+
+ ui->transitions->blockSignals(false);
+}
+
+int OBSBasic::TransitionCount()
+{
+ int idx = 0;
+ for (int i = 0; i < ui->transitions->count(); i++) {
+ QVariant v = ui->transitions->itemData(i);
+ if (!v.toString().isEmpty()) {
+ idx = i;
+ break;
+ }
+ }
+
+ /* should always have at least fade and cut due to them being
+ * defaults */
+ assert(idx != 0);
+ return idx - 1; /* remove separator from equation */
+}
+
+int OBSBasic::AddTransitionBeforeSeparator(const QString &name,
+ obs_source_t *source)
+{
+ int idx = TransitionCount();
+ ui->transitions->blockSignals(true);
+ ui->transitions->insertItem(idx, name,
+ QVariant::fromValue(OBSSource(source)));
+ ui->transitions->blockSignals(false);
+ return idx;
}
void OBSBasic::AddQuickTransitionHotkey(QuickTransition *qt)
@@ -171,15 +223,12 @@ void OBSBasic::CreateDefaultQuickTransitions()
{
/* non-configurable transitions are always available, so add them
* to the "default quick transitions" list */
- quickTransitions.emplace_back(GetTransitionComboItem(ui->transitions,
- 0),
- 300, quickTransitionIdCounter++);
- quickTransitions.emplace_back(GetTransitionComboItem(ui->transitions,
- 1),
- 300, quickTransitionIdCounter++);
- quickTransitions.emplace_back(GetTransitionComboItem(ui->transitions,
- 1),
- 300, quickTransitionIdCounter++, true);
+ quickTransitions.emplace_back(cutTransition, 300,
+ quickTransitionIdCounter++);
+ quickTransitions.emplace_back(fadeTransition, 300,
+ quickTransitionIdCounter++);
+ quickTransitions.emplace_back(fadeTransition, 300,
+ quickTransitionIdCounter++, true);
}
void OBSBasic::LoadQuickTransitions(obs_data_array_t *array)
@@ -246,9 +295,11 @@ obs_source_t *OBSBasic::FindTransition(const char *name)
{
for (int i = 0; i < ui->transitions->count(); i++) {
OBSSource tr = ui->transitions->itemData(i).value();
+ if (!tr)
+ continue;
const char *trName = obs_source_get_name(tr);
- if (strcmp(trName, name) == 0)
+ if (trName && *trName && strcmp(trName, name) == 0)
return tr;
}
@@ -408,6 +459,10 @@ static inline void SetComboTransition(QComboBox *combo, obs_source_t *tr)
void OBSBasic::SetTransition(OBSSource transition)
{
obs_source_t *oldTransition = obs_get_output_source(0);
+ obs_source_release(oldTransition);
+
+ if (transition == oldTransition)
+ return;
if (oldTransition && transition) {
obs_transition_swap_begin(transition, oldTransition);
@@ -419,17 +474,15 @@ void OBSBasic::SetTransition(OBSSource transition)
obs_set_output_source(0, transition);
}
- if (oldTransition)
- obs_source_release(oldTransition);
-
bool fixed = transition ? obs_transition_fixed(transition) : false;
ui->transitionDurationLabel->setVisible(!fixed);
ui->transitionDuration->setVisible(!fixed);
bool configurable = obs_source_configurable(transition);
- ui->transitionRemove->setEnabled(configurable);
ui->transitionProps->setEnabled(configurable);
+ SetComboTransition(ui->transitions, transition);
+
if (api)
api->on_event(OBS_FRONTEND_EVENT_TRANSITION_CHANGED);
}
@@ -442,17 +495,21 @@ OBSSource OBSBasic::GetCurrentTransition()
void OBSBasic::on_transitions_currentIndexChanged(int)
{
OBSSource transition = GetCurrentTransition();
- SetTransition(transition);
+
+ if (transition)
+ SetTransition(transition);
+ else
+ AddTransition(ui->transitions->currentData().value());
}
-void OBSBasic::AddTransition()
+void OBSBasic::AddTransition(QString id)
{
- QAction *action = reinterpret_cast(sender());
- QString idStr = action->property("id").toString();
+ if (id.isEmpty())
+ return;
string name;
QString placeHolderText =
- QT_UTF8(obs_source_get_display_name(QT_TO_UTF8(idStr)));
+ QT_UTF8(obs_source_get_display_name(QT_TO_UTF8(id)));
QString format = placeHolderText + " (%1)";
obs_source_t *source = nullptr;
int i = 1;
@@ -471,7 +528,7 @@ void OBSBasic::AddTransition()
OBSMessageBox::warning(this,
QTStr("NoNameEntered.Title"),
QTStr("NoNameEntered.Text"));
- AddTransition();
+ AddTransition(id);
return;
}
@@ -480,17 +537,16 @@ void OBSBasic::AddTransition()
OBSMessageBox::warning(this, QTStr("NameExists.Title"),
QTStr("NameExists.Text"));
- AddTransition();
+ AddTransition(id);
return;
}
- source = obs_source_create_private(QT_TO_UTF8(idStr),
- name.c_str(), NULL);
+ source = obs_source_create_private(QT_TO_UTF8(id), name.c_str(),
+ NULL);
InitTransition(source);
- ui->transitions->addItem(
- QT_UTF8(name.c_str()),
- QVariant::fromValue(OBSSource(source)));
- ui->transitions->setCurrentIndex(ui->transitions->count() - 1);
+ int idx = AddTransitionBeforeSeparator(QT_UTF8(name.c_str()),
+ source);
+ ui->transitions->setCurrentIndex(idx);
CreatePropertiesWindow(source);
obs_source_release(source);
@@ -500,34 +556,13 @@ void OBSBasic::AddTransition()
ClearQuickTransitionWidgets();
RefreshQuickTransitions();
+ } else {
+ obs_source_t *transition = obs_get_output_source(0);
+ SetComboTransition(ui->transitions, transition);
+ obs_source_release(transition);
}
}
-void OBSBasic::on_transitionAdd_clicked()
-{
- bool foundConfigurableTransitions = false;
- QMenu menu(this);
- size_t idx = 0;
- const char *id;
-
- while (obs_enum_transition_types(idx++, &id)) {
- if (obs_is_source_configurable(id)) {
- const char *name = obs_source_get_display_name(id);
- QAction *action = new QAction(name, this);
- action->setProperty("id", id);
-
- connect(action, SIGNAL(triggered()), this,
- SLOT(AddTransition()));
-
- menu.addAction(action);
- foundConfigurableTransitions = true;
- }
- }
-
- if (foundConfigurableTransitions)
- menu.exec(QCursor::pos());
-}
-
void OBSBasic::on_transitionRemove_clicked()
{
OBSSource tr = GetCurrentTransition();
@@ -550,7 +585,15 @@ void OBSBasic::on_transitionRemove_clicked()
}
}
+ ui->transitions->blockSignals(true);
ui->transitions->removeItem(idx);
+ ui->transitions->setCurrentIndex(-1);
+ ui->transitions->blockSignals(false);
+
+ int bottomIdx = TransitionCount() - 1;
+ if (idx > bottomIdx)
+ idx = bottomIdx;
+ ui->transitions->setCurrentIndex(idx);
if (api)
api->on_event(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED);
@@ -624,6 +667,11 @@ void OBSBasic::on_transitionProps_clicked()
action->setProperty("transition", QVariant::fromValue(source));
menu.addAction(action);
+ action = new QAction(QTStr("Remove"), &menu);
+ connect(action, SIGNAL(triggered()), this,
+ SLOT(on_transitionRemove_clicked()));
+ menu.addAction(action);
+
action = new QAction(QTStr("Properties"), &menu);
connect(action, &QAction::triggered, properties);
menu.addAction(action);
@@ -708,10 +756,12 @@ void OBSBasic::SetCurrentScene(OBSSource scene, bool force)
UpdateSceneSelection(scene);
- bool userSwitched = (!force && !disableSaving);
- blog(LOG_INFO, "%s to scene '%s'",
- userSwitched ? "User switched" : "Switched",
- obs_source_get_name(scene));
+ if (scene) {
+ bool userSwitched = (!force && !disableSaving);
+ blog(LOG_INFO, "%s to scene '%s'",
+ userSwitched ? "User switched" : "Switched",
+ obs_source_get_name(scene));
+ }
}
void OBSBasic::CreateProgramDisplay()
@@ -915,6 +965,8 @@ void OBSBasic::TBarChanged(int value)
OBSSource transition = obs_get_output_source(0);
obs_source_release(transition);
+ tBar->setValue(value);
+
if (!tBarActive) {
OBSSource sceneSource = GetCurrentSceneSource();
OBSSource tBarTr = GetOverrideTransition(sceneSource);
@@ -985,9 +1037,11 @@ QMenu *OBSBasic::CreatePerSceneTransitionMenu()
}
OBSSource tr = GetTransitionComboItem(ui->transitions, idx);
- const char *name = obs_source_get_name(tr);
- obs_data_set_string(data, "transition", name);
+ if (tr) {
+ const char *name = obs_source_get_name(tr);
+ obs_data_set_string(data, "transition", name);
+ }
};
auto setDuration = [this](int duration) {
@@ -1007,6 +1061,8 @@ QMenu *OBSBasic::CreatePerSceneTransitionMenu()
if (i >= 0) {
OBSSource tr;
tr = GetTransitionComboItem(ui->transitions, i);
+ if (!tr)
+ continue;
name = obs_source_get_name(tr);
}
@@ -1062,10 +1118,9 @@ QMenu *OBSBasic::CreateTransitionMenu(QWidget *parent, QuickTransition *qt)
this, &OBSBasic::QuickTransitionChangeDuration);
}
- tr = GetTransitionComboItem(ui->transitions, 1);
+ tr = fadeTransition;
action = menu->addAction(QTStr("FadeToBlack"));
- action->setProperty("transition_index", 1);
action->setProperty("fadeToBlack", true);
if (qt) {
@@ -1082,6 +1137,9 @@ QMenu *OBSBasic::CreateTransitionMenu(QWidget *parent, QuickTransition *qt)
for (int i = 0; i < ui->transitions->count(); i++) {
tr = GetTransitionComboItem(ui->transitions, i);
+ if (!tr)
+ continue;
+
action = menu->addAction(obs_source_get_name(tr));
action->setProperty("transition_index", i);
@@ -1151,12 +1209,18 @@ void OBSBasic::AddQuickTransition()
{
int trIdx = sender()->property("transition_index").toInt();
QSpinBox *duration = sender()->property("duration").value();
- bool toBlack = sender()->property("fadeToBlack").value();
- OBSSource transition = GetTransitionComboItem(ui->transitions, trIdx);
+ bool fadeToBlack = sender()->property("fadeToBlack").value();
+ OBSSource transition =
+ fadeToBlack ? OBSSource(fadeTransition)
+ : GetTransitionComboItem(ui->transitions, trIdx);
+
+ if (!transition)
+ return;
+
int id = quickTransitionIdCounter++;
quickTransitions.emplace_back(transition, duration->value(), id,
- toBlack);
+ fadeToBlack);
AddQuickTransitionId(id);
int idx = (int)quickTransitions.size() - 1;
@@ -1202,11 +1266,18 @@ void OBSBasic::QuickTransitionChange()
{
int id = sender()->property("id").toInt();
int trIdx = sender()->property("transition_index").toInt();
+ bool fadeToBlack = sender()->property("fadeToBlack").value();
QuickTransition *qt = GetQuickTransition(id);
if (qt) {
- qt->source = GetTransitionComboItem(ui->transitions, trIdx);
- ResetQuickTransitionText(qt);
+ OBSSource tr = fadeToBlack
+ ? OBSSource(fadeTransition)
+ : GetTransitionComboItem(ui->transitions,
+ trIdx);
+ if (tr) {
+ qt->source = tr;
+ ResetQuickTransitionText(qt);
+ }
}
}
@@ -1480,7 +1551,7 @@ obs_data_array_t *OBSBasic::SaveTransitions()
for (int i = 0; i < ui->transitions->count(); i++) {
OBSSource tr = ui->transitions->itemData(i).value();
- if (!obs_source_configurable(tr))
+ if (!tr || !obs_source_configurable(tr))
continue;
obs_data_t *sourceData = obs_data_create();
@@ -1514,11 +1585,7 @@ void OBSBasic::LoadTransitions(obs_data_array_t *transitions)
obs_source_create_private(id, name, settings);
if (!obs_obj_invalid(source)) {
InitTransition(source);
- ui->transitions->addItem(
- QT_UTF8(name),
- QVariant::fromValue(OBSSource(source)));
- ui->transitions->setCurrentIndex(
- ui->transitions->count() - 1);
+ AddTransitionBeforeSeparator(QT_UTF8(name), source);
}
obs_data_release(settings);
diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp
index cb1a290..89eacc0 100644
--- a/UI/window-basic-main.cpp
+++ b/UI/window-basic-main.cpp
@@ -30,6 +30,7 @@
#include
#include
#include
+#include
#include
#include
@@ -52,10 +53,13 @@
#include "window-projector.hpp"
#include "window-remux.hpp"
#include "qt-wrappers.hpp"
+#include "context-bar-controls.hpp"
+#include "obs-proxy-style.hpp"
#include "display-helpers.hpp"
#include "volume-control.hpp"
#include "remote-text.hpp"
#include "ui-validation.hpp"
+#include "media-controls.hpp"
#include
#include
@@ -194,7 +198,6 @@ void assignDockToggle(QDockWidget *dock, QAction *action)
}
extern void RegisterTwitchAuth();
-extern void RegisterMixerAuth();
extern void RegisterRestreamAuth();
extern void RegisterRedditAuth();
@@ -209,9 +212,6 @@ OBSBasic::OBSBasic(QWidget *parent)
#if TWITCH_ENABLED
RegisterTwitchAuth();
#endif
-#if MIXER_ENABLED
- RegisterMixerAuth();
-#endif
#if RESTREAM_ENABLED
RegisterRestreamAuth();
#endif
@@ -229,12 +229,21 @@ OBSBasic::OBSBasic(QWidget *parent)
ui->setupUi(this);
ui->previewDisabledWidget->setVisible(false);
+ ui->contextContainer->setStyle(new OBSProxyStyle);
+
+ if (!QSslSocket::supportsSsl()) {
+ blog(LOG_ERROR, "SSL library build version: %s (%lld)", QSslSocket::sslLibraryBuildVersionString().toUtf8().data(), QSslSocket::sslLibraryBuildVersionNumber());
+ blog(LOG_ERROR, "SSL library version: %s (%lld)", QSslSocket::sslLibraryVersionString().toUtf8().data(), QSslSocket::sslLibraryVersionNumber());
+ throw "SSL is unavailable";
+ }
startingDockLayout = saveState();
statsDock = new OBSDock();
statsDock->setObjectName(QStringLiteral("statsDock"));
- statsDock->setFeatures(QDockWidget::AllDockWidgetFeatures);
+ statsDock->setFeatures(QDockWidget::DockWidgetClosable |
+ QDockWidget::DockWidgetMovable |
+ QDockWidget::DockWidgetFloatable);
statsDock->setWindowTitle(QTStr("Basic.Stats"));
addDockWidget(Qt::BottomDockWidgetArea, statsDock);
statsDock->setVisible(false);
@@ -312,12 +321,12 @@ OBSBasic::OBSBasic(QWidget *parent)
connect(diskFullTimer, SIGNAL(timeout()), this,
SLOT(CheckDiskSpaceRemaining()));
- QAction *renameScene = new QAction(ui->scenesDock);
+ renameScene = new QAction(ui->scenesDock);
renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut);
connect(renameScene, SIGNAL(triggered()), this, SLOT(EditSceneName()));
ui->scenesDock->addAction(renameScene);
- QAction *renameSource = new QAction(ui->sourcesDock);
+ renameSource = new QAction(ui->sourcesDock);
renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut);
connect(renameSource, SIGNAL(triggered()), this,
SLOT(EditSceneItemName()));
@@ -337,6 +346,10 @@ OBSBasic::OBSBasic(QWidget *parent)
renameSource->setShortcut({Qt::Key_F2});
#endif
+#ifdef __linux__
+ ui->actionE_xit->setShortcut(Qt::CTRL + Qt::Key_Q);
+#endif
+
auto addNudge = [this](const QKeySequence &seq, const char *s) {
QAction *nudge = new QAction(ui->preview);
nudge->setShortcut(seq);
@@ -367,6 +380,8 @@ OBSBasic::OBSBasic(QWidget *parent)
QPoint curPos;
+ UpdateContextBar();
+
//restore parent window geometry
const char *geometry = config_get_string(App()->GlobalConfig(),
"BasicWindow", "geometry");
@@ -392,6 +407,7 @@ OBSBasic::OBSBasic(QWidget *parent)
}
QPoint curSize(width(), height());
+
QPoint statsDockSize(statsDock->width(), statsDock->height());
QPoint statsDockPos = curSize / 2 - statsDockSize / 2;
QPoint newPos = curPos + statsDockPos;
@@ -415,11 +431,8 @@ OBSBasic::OBSBasic(QWidget *parent)
connect(ui->enablePreviewButton, SIGNAL(clicked()), this,
SLOT(TogglePreview()));
- connect(ui->scenes->model(),
- SIGNAL(rowsMoved(QModelIndex, int, int, QModelIndex, int)),
- this,
- SLOT(ScenesReordered(const QModelIndex &, int, int,
- const QModelIndex &, int)));
+ connect(ui->scenes, SIGNAL(scenesReordered()), this,
+ SLOT(ScenesReordered()));
#if REDDIT_ENABLED
auto *action = ui->menuTools->addAction(
@@ -609,6 +622,7 @@ obs_data_array_t *OBSBasic::SaveProjectors()
obs_data_t *data = obs_data_create();
ProjectorType type = projector->GetProjectorType();
+
switch (type) {
case ProjectorType::Scene:
case ProjectorType::Source: {
@@ -620,11 +634,20 @@ obs_data_array_t *OBSBasic::SaveProjectors()
default:
break;
}
+
obs_data_set_int(data, "monitor", projector->GetMonitor());
obs_data_set_int(data, "type", static_cast(type));
obs_data_set_string(
data, "geometry",
projector->saveGeometry().toBase64().constData());
+
+ if (projector->IsAlwaysOnTopOverridden())
+ obs_data_set_bool(data, "alwaysOnTop",
+ projector->IsAlwaysOnTop());
+
+ obs_data_set_bool(data, "alwaysOnTopOverridden",
+ projector->IsAlwaysOnTopOverridden());
+
obs_data_array_push_back(savedProjectors, data);
obs_data_release(data);
};
@@ -705,6 +728,16 @@ static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent)
const char *name = obs_source_get_name(source);
blog(LOG_INFO, "[Loaded global audio device]: '%s'", name);
obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1);
+ obs_monitoring_type monitoring_type =
+ obs_source_get_monitoring_type(source);
+ if (monitoring_type != OBS_MONITORING_TYPE_NONE) {
+ const char *type = (monitoring_type ==
+ OBS_MONITORING_TYPE_MONITOR_ONLY)
+ ? "monitor only"
+ : "monitor and output";
+
+ blog(LOG_INFO, " - monitoring: %s", type);
+ }
obs_source_release(source);
}
@@ -813,6 +846,10 @@ void OBSBasic::LoadSavedProjectors(obs_data_array_t *array)
info->geometry =
std::string(obs_data_get_string(data, "geometry"));
info->name = std::string(obs_data_get_string(data, "name"));
+ info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop");
+ info->alwaysOnTopOverridden =
+ obs_data_get_bool(data, "alwaysOnTopOverridden");
+
savedProjectorsArray.emplace_back(info);
obs_data_release(data);
@@ -900,6 +937,14 @@ void OBSBasic::Load(const char *file)
ClearSceneData();
InitDefaultTransitions();
+ ClearContextBar();
+
+ if (devicePropertiesThread && devicePropertiesThread->isRunning()) {
+ devicePropertiesThread->wait();
+ devicePropertiesThread.reset();
+ }
+
+ QApplication::sendPostedEvents(this);
obs_data_t *modulesObj = obs_data_get_obj(data, "modules");
if (api)
@@ -1087,6 +1132,12 @@ void OBSBasic::Load(const char *file)
opt_start_replaybuffer = false;
}
+ if (opt_start_virtualcam) {
+ QMetaObject::invokeMethod(this, "StartVirtualCam",
+ Qt::QueuedConnection);
+ opt_start_virtualcam = false;
+ }
+
copyStrings.clear();
copyFiltersString = nullptr;
@@ -1139,6 +1190,9 @@ bool OBSBasic::LoadService()
obs_data_t *data =
obs_data_create_from_json_file_safe(serviceJsonPath, "bak");
+ if (!data)
+ return false;
+
obs_data_set_default_string(data, "type", "rtmp_common");
type = obs_data_get_string(data, "type");
@@ -1374,7 +1428,7 @@ bool OBSBasic::InitBasicConfigDefaults()
config_set_default_uint(basicConfig, "Video", "FPSDen", 1);
config_set_default_string(basicConfig, "Video", "ScaleType", "bicubic");
config_set_default_string(basicConfig, "Video", "ColorFormat", "NV12");
- config_set_default_string(basicConfig, "Video", "ColorSpace", "601");
+ config_set_default_string(basicConfig, "Video", "ColorSpace", "709");
config_set_default_string(basicConfig, "Video", "ColorRange",
"Partial");
@@ -1384,7 +1438,7 @@ bool OBSBasic::InitBasicConfigDefaults()
basicConfig, "Audio", "MonitoringDeviceName",
Str("Basic.Settings.Advanced.Audio.MonitoringDevice"
".Default"));
- config_set_default_uint(basicConfig, "Audio", "SampleRate", 44100);
+ config_set_default_uint(basicConfig, "Audio", "SampleRate", 48000);
config_set_default_string(basicConfig, "Audio", "ChannelSetup",
"Stereo");
config_set_default_double(basicConfig, "Audio", "MeterDecayRate",
@@ -1527,6 +1581,20 @@ void OBSBasic::ReplayBufferClicked()
StartReplayBuffer();
};
+void OBSBasic::AddVCamButton()
+{
+ vcamButton = new ReplayBufferButton(QTStr("Basic.Main.StartVirtualCam"),
+ this);
+ vcamButton->setCheckable(true);
+ connect(vcamButton.data(), &QPushButton::clicked, this,
+ &OBSBasic::VCamButtonClicked);
+
+ vcamButton->setProperty("themeID", "vcamButton");
+ ui->buttonsVLayout->insertWidget(2, vcamButton);
+ setTabOrder(ui->recordButton, vcamButton);
+ setTabOrder(vcamButton, ui->modeSwitch);
+}
+
void OBSBasic::ResetOutputs()
{
ProfileScope("OBSBasic::ResetOutputs");
@@ -1550,12 +1618,18 @@ void OBSBasic::ResetOutputs()
&QPushButton::clicked, this,
&OBSBasic::ReplayBufferClicked);
+ replayBufferButton->setSizePolicy(QSizePolicy::Ignored,
+ QSizePolicy::Fixed);
+
replayLayout = new QHBoxLayout(this);
replayLayout->addWidget(replayBufferButton);
replayBufferButton->setProperty("themeID",
"replayBufferButton");
ui->buttonsVLayout->insertLayout(2, replayLayout);
+ setTabOrder(ui->recordButton, replayBufferButton);
+ setTabOrder(replayBufferButton,
+ ui->buttonsVLayout->itemAt(3)->widget());
}
if (sysTrayReplayBuffer)
@@ -1651,6 +1725,13 @@ void OBSBasic::OBSInit()
cef = obs_browser_init_panel();
#endif
+ obs_data_t *obsData = obs_get_private_data();
+ vcamEnabled = obs_data_get_bool(obsData, "vcamEnabled");
+ if (vcamEnabled) {
+ AddVCamButton();
+ }
+ obs_data_release(obsData);
+
InitBasicConfigDefaults2();
CheckForSimpleModeX264Fallback();
@@ -1672,6 +1753,15 @@ void OBSBasic::OBSInit()
editPropertiesMode = config_get_bool(
App()->GlobalConfig(), "BasicWindow", "EditPropertiesMode");
+ if (!opt_studio_mode) {
+ SetPreviewProgramMode(config_get_bool(App()->GlobalConfig(),
+ "BasicWindow",
+ "PreviewProgramMode"));
+ } else {
+ SetPreviewProgramMode(true);
+ opt_studio_mode = false;
+ }
+
#define SET_VISIBILITY(name, control) \
do { \
if (config_has_user_value(App()->GlobalConfig(), \
@@ -1690,6 +1780,18 @@ void OBSBasic::OBSInit()
GetGlobalConfig(), "BasicWindow", "ShowSourceIcons");
ui->toggleSourceIcons->setChecked(sourceIconsVisible);
+ if (config_has_user_value(App()->GlobalConfig(), "BasicWindow",
+ "ShowContextToolbars")) {
+ bool visible = config_get_bool(App()->GlobalConfig(),
+ "BasicWindow",
+ "ShowContextToolbars");
+ ui->toggleContextBar->setChecked(visible);
+ ui->contextContainer->setVisible(visible);
+ } else {
+ ui->toggleContextBar->setChecked(true);
+ ui->contextContainer->setVisible(true);
+ }
+
{
ProfileScope("OBSBasic::Load");
disableSaving--;
@@ -1772,6 +1874,7 @@ void OBSBasic::OBSInit()
const char *dockStateStr = config_get_string(
App()->GlobalConfig(), "BasicWindow", "DockState");
+
if (!dockStateStr) {
on_resetUI_triggered();
} else {
@@ -1806,6 +1909,10 @@ void OBSBasic::OBSInit()
SystemTray(true);
#endif
+#ifdef __APPLE__
+ disableColorSpaceConversion(this);
+#endif
+
bool has_last_version = config_has_user_value(App()->GlobalConfig(),
"General", "LastVersion");
bool first_run =
@@ -1818,23 +1925,9 @@ void OBSBasic::OBSInit()
}
#if !REDDIT_ENABLED
- if (!first_run && !has_last_version && !Active()) {
- QString msg;
- msg = QTStr("Basic.FirstStartup.RunWizard");
-
- QMessageBox::StandardButton button = OBSMessageBox::question(
- this, QTStr("Basic.AutoConfig"), msg);
-
- if (button == QMessageBox::Yes) {
- QMetaObject::invokeMethod(this,
- "on_autoConfigure_triggered",
- Qt::QueuedConnection);
- } else {
- msg = QTStr("Basic.FirstStartup.RunWizard.NoClicked");
- OBSMessageBox::information(
- this, QTStr("Basic.AutoConfig"), msg);
- }
- }
+ if (!first_run && !has_last_version && !Active())
+ QMetaObject::invokeMethod(this, "on_autoConfigure_triggered",
+ Qt::QueuedConnection);
#endif
ToggleMixerLayout(config_get_bool(App()->GlobalConfig(), "BasicWindow",
@@ -1861,15 +1954,6 @@ void OBSBasic::OBSInit()
ui->sources->UpdateIcons();
- if (!opt_studio_mode) {
- SetPreviewProgramMode(config_get_bool(App()->GlobalConfig(),
- "BasicWindow",
- "PreviewProgramMode"));
- } else {
- SetPreviewProgramMode(true);
- opt_studio_mode = false;
- }
-
#if !defined(_WIN32) && !defined(__APPLE__)
delete ui->actionShowCrashLogs;
delete ui->actionUploadLastCrashLog;
@@ -1879,10 +1963,15 @@ void OBSBasic::OBSInit()
ui->actionUploadLastCrashLog = nullptr;
ui->menuCrashLogs = nullptr;
ui->actionCheckForUpdates = nullptr;
+#elif _WIN32 || __APPLE__
+ if (App()->IsUpdaterDisabled())
+ ui->actionCheckForUpdates->setEnabled(false);
#endif
OnFirstLoad();
+ activateWindow();
+
#ifdef __APPLE__
QMetaObject::invokeMethod(this, "DeferredSysTrayLoad",
Qt::QueuedConnection, Q_ARG(int, 10));
@@ -1898,18 +1987,24 @@ void OBSBasic::OnFirstLoad()
/* Attempt to load init screen if available */
if (cef) {
WhatsNewInfoThread *wnit = new WhatsNewInfoThread();
- if (wnit) {
- connect(wnit, &WhatsNewInfoThread::Result, this,
- &OBSBasic::ReceivedIntroJson);
- }
- if (wnit) {
- introCheckThread.reset(wnit);
- introCheckThread->start();
- }
+ connect(wnit, &WhatsNewInfoThread::Result, this,
+ &OBSBasic::ReceivedIntroJson);
+
+ introCheckThread.reset(wnit);
+ introCheckThread->start();
}
#endif
Auth::Load();
+
+ bool showLogViewerOnStartup = config_get_bool(
+ App()->GlobalConfig(), "LogViewer", "ShowLogStartup");
+
+ if (showLogViewerOnStartup) {
+ if (!logView)
+ logView = new OBSLogViewer();
+ logView->show();
+ }
}
void OBSBasic::DeferredSysTrayLoad(int requeueCount)
@@ -2011,14 +2106,13 @@ void OBSBasic::ReceivedIntroJson(const QString &text)
WhatsNewBrowserInitThread *wnbit =
new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str()));
- if (wnbit) {
- connect(wnbit, &WhatsNewBrowserInitThread::Result, this,
- &OBSBasic::ShowWhatsNew);
- }
- if (wnbit) {
- whatsNewInitThread.reset(wnbit);
- whatsNewInitThread->start();
- }
+
+ connect(wnbit, &WhatsNewBrowserInitThread::Result, this,
+ &OBSBasic::ShowWhatsNew);
+
+ whatsNewInitThread.reset(wnbit);
+ whatsNewInitThread->start();
+
#else
UNUSED_PARAMETER(text);
#endif
@@ -2263,6 +2357,23 @@ void OBSBasic::CreateHotkeys()
LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer",
"OBSBasic.StopReplayBuffer");
+ if (vcamEnabled) {
+ vcamHotkeys = obs_hotkey_pair_register_frontend(
+ "OBSBasic.StartVirtualCam",
+ Str("Basic.Main.StartVirtualCam"),
+ "OBSBasic.StopVirtualCam",
+ Str("Basic.Main.StopVirtualCam"),
+ MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(),
+ basic.StartVirtualCam,
+ "Starting virtual camera"),
+ MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(),
+ basic.StopVirtualCam,
+ "Stopping virtual camera"),
+ this, this);
+ LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam",
+ "OBSBasic.StopVirtualCam");
+ }
+
togglePreviewHotkeys = obs_hotkey_pair_register_frontend(
"OBSBasic.EnablePreview",
Str("Basic.Main.PreviewConextMenu.Enable"),
@@ -2274,6 +2385,17 @@ void OBSBasic::CreateHotkeys()
this, this);
LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview",
"OBSBasic.DisablePreview");
+
+ contextBarHotkeys = obs_hotkey_pair_register_frontend(
+ "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"),
+ "OBSBasic.HideContextBar", Str("Basic.Main.HideContextBar"),
+ MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(),
+ basic.ShowContextBar, "Showing Context Bar"),
+ MAKE_CALLBACK(basic.ui->contextContainer->isVisible(),
+ basic.HideContextBar, "Hiding Context Bar"),
+ this, this);
+ LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar",
+ "OBSBasic.HideContextBar");
#undef MAKE_CALLBACK
auto togglePreviewProgram = [](void *data, obs_hotkey_id,
@@ -2314,6 +2436,31 @@ void OBSBasic::CreateHotkeys()
"OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"),
resetStats, this);
LoadHotkey(statsHotkey, "OBSBasic.ResetStats");
+
+ auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *,
+ bool pressed) {
+ if (pressed)
+ QMetaObject::invokeMethod(static_cast(data),
+ "Screenshot",
+ Qt::QueuedConnection);
+ };
+
+ screenshotHotkey = obs_hotkey_register_frontend(
+ "OBSBasic.Screenshot", Str("Screenshot"), screenshot, this);
+ LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot");
+
+ auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *,
+ bool pressed) {
+ if (pressed)
+ QMetaObject::invokeMethod(static_cast(data),
+ "ScreenshotSelectedSource",
+ Qt::QueuedConnection);
+ };
+
+ sourceScreenshotHotkey = obs_hotkey_register_frontend(
+ "OBSBasic.SelectedSourceScreenshot",
+ Str("Screenshot.SourceHotkey"), screenshotSource, this);
+ LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot");
}
void OBSBasic::ClearHotkeys()
@@ -2327,6 +2474,8 @@ void OBSBasic::ClearHotkeys()
obs_hotkey_unregister(togglePreviewProgramHotkey);
obs_hotkey_unregister(transitionHotkey);
obs_hotkey_unregister(statsHotkey);
+ obs_hotkey_unregister(screenshotHotkey);
+ obs_hotkey_unregister(sourceScreenshotHotkey);
}
OBSBasic::~OBSBasic()
@@ -2337,6 +2486,8 @@ OBSBasic::~OBSBasic()
if (updateCheckThread && updateCheckThread->isRunning())
updateCheckThread->wait();
+ delete screenshotData;
+ delete logView;
delete multiviewProjectorMenu;
delete previewProjector;
delete studioProgramProjector;
@@ -2546,8 +2697,12 @@ void OBSBasic::UpdatePreviewScalingMenu()
void OBSBasic::CreateInteractionWindow(obs_source_t *source)
{
+ bool closed = true;
if (interaction)
- interaction->close();
+ closed = interaction->close();
+
+ if (!closed)
+ return;
interaction = new OBSBasicInteraction(this, source);
interaction->Init();
@@ -2556,8 +2711,12 @@ void OBSBasic::CreateInteractionWindow(obs_source_t *source)
void OBSBasic::CreatePropertiesWindow(obs_source_t *source)
{
+ bool closed = true;
if (properties)
- properties->close();
+ closed = properties->close();
+
+ if (!closed)
+ return;
properties = new OBSBasicProperties(this, source);
properties->Init();
@@ -2566,8 +2725,12 @@ void OBSBasic::CreatePropertiesWindow(obs_source_t *source)
void OBSBasic::CreateFiltersWindow(obs_source_t *source)
{
+ bool closed = true;
if (filters)
- filters->close();
+ closed = filters->close();
+
+ if (!closed)
+ return;
filters = new OBSBasicFilters(this, source);
filters->Init();
@@ -2608,11 +2771,6 @@ void OBSBasic::AddScene(OBSSource source)
container.handlers.assign({
std::make_shared(handler, "item_add",
OBSBasic::SceneItemAdded, this),
- std::make_shared(handler, "item_select",
- OBSBasic::SceneItemSelected, this),
- std::make_shared(handler, "item_deselect",
- OBSBasic::SceneItemDeselected,
- this),
std::make_shared(handler, "reorder",
OBSBasic::SceneReordered, this),
std::make_shared(handler, "refresh",
@@ -2780,16 +2938,123 @@ void OBSBasic::RenameSources(OBSSource source, QString newName,
obs_scene_t *scene = obs_scene_from_source(source);
if (scene)
OBSProjector::UpdateMultiviewProjectors();
+
+ UpdateContextBar();
}
-void OBSBasic::SelectSceneItem(OBSScene scene, OBSSceneItem item, bool select)
+void OBSBasic::ClearContextBar()
{
- SignalBlocker sourcesSignalBlocker(ui->sources);
+ QLayoutItem *la = ui->emptySpace->layout()->itemAt(0);
+ if (la) {
+ delete la->widget();
+ ui->emptySpace->layout()->removeItem(la);
+ }
+}
- if (scene != GetCurrentScene() || ignoreSelectionUpdate)
- return;
+static bool is_network_media_source(obs_source_t *source, const char *id)
+{
+ if (strcmp(id, "ffmpeg_source") != 0)
+ return false;
+
+ obs_data_t *s = obs_source_get_settings(source);
+ bool is_local_file = obs_data_get_bool(s, "is_local_file");
+ obs_data_release(s);
+
+ return !is_local_file;
+}
+
+void OBSBasic::UpdateContextBar()
+{
+ OBSSceneItem item = GetCurrentSceneItem();
+
+ ClearContextBar();
+
+ if (item) {
+ obs_source_t *source = obs_sceneitem_get_source(item);
+ const char *id = obs_source_get_unversioned_id(source);
+ uint32_t flags = obs_source_get_output_flags(source);
+
+ if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) {
+ if (!is_network_media_source(source, id)) {
+ MediaControls *mediaControls =
+ new MediaControls(ui->emptySpace);
+ mediaControls->SetSource(source);
+
+ ui->emptySpace->layout()->addWidget(
+ mediaControls);
+ }
+ } else if (strcmp(id, "browser_source") == 0) {
+ BrowserToolbar *c =
+ new BrowserToolbar(ui->emptySpace, source);
+ ui->emptySpace->layout()->addWidget(c);
+
+ } else if (strcmp(id, "wasapi_input_capture") == 0 ||
+ strcmp(id, "wasapi_output_capture") == 0 ||
+ strcmp(id, "coreaudio_input_capture") == 0 ||
+ strcmp(id, "coreaudio_output_capture") == 0 ||
+ strcmp(id, "pulse_input_capture") == 0 ||
+ strcmp(id, "pulse_output_capture") == 0 ||
+ strcmp(id, "alsa_input_capture") == 0) {
+ AudioCaptureToolbar *c =
+ new AudioCaptureToolbar(ui->emptySpace, source);
+ c->Init();
+ ui->emptySpace->layout()->addWidget(c);
+
+ } else if (strcmp(id, "window_capture") == 0 ||
+ strcmp(id, "xcomposite_input") == 0) {
+ WindowCaptureToolbar *c = new WindowCaptureToolbar(
+ ui->emptySpace, source);
+ c->Init();
+ ui->emptySpace->layout()->addWidget(c);
+
+ } else if (strcmp(id, "monitor_capture") == 0 ||
+ strcmp(id, "display_capture") == 0 ||
+ strcmp(id, "xshm_input") == 0) {
+ DisplayCaptureToolbar *c = new DisplayCaptureToolbar(
+ ui->emptySpace, source);
+ c->Init();
+ ui->emptySpace->layout()->addWidget(c);
+
+ } else if (strcmp(id, "dshow_input") == 0) {
+ DeviceCaptureToolbar *c = new DeviceCaptureToolbar(
+ ui->emptySpace, source);
+ ui->emptySpace->layout()->addWidget(c);
+
+ } else if (strcmp(id, "game_capture") == 0) {
+ GameCaptureToolbar *c =
+ new GameCaptureToolbar(ui->emptySpace, source);
+ ui->emptySpace->layout()->addWidget(c);
+
+ } else if (strcmp(id, "image_source") == 0) {
+ ImageSourceToolbar *c =
+ new ImageSourceToolbar(ui->emptySpace, source);
+ ui->emptySpace->layout()->addWidget(c);
+
+ } else if (strcmp(id, "color_source") == 0) {
+ ColorSourceToolbar *c =
+ new ColorSourceToolbar(ui->emptySpace, source);
+ ui->emptySpace->layout()->addWidget(c);
+
+ } else if (strcmp(id, "text_ft2_source") == 0 ||
+ strcmp(id, "text_gdiplus") == 0) {
+ TextSourceToolbar *c =
+ new TextSourceToolbar(ui->emptySpace, source);
+ ui->emptySpace->layout()->addWidget(c);
+ }
+
+ const char *name = obs_source_get_name(source);
+ ui->contextSourceLabel->setText(name);
+
+ ui->sourceFiltersButton->setEnabled(true);
+ ui->sourcePropertiesButton->setEnabled(
+ obs_source_configurable(source));
+ } else {
+ ui->contextSourceLabel->setText(
+ QTStr("ContextBar.NoSelectedSource"));
- ui->sources->SelectItem(item, select);
+ ui->sourceFiltersButton->setEnabled(false);
+ ui->sourcePropertiesButton->setEnabled(false);
+ }
}
static inline bool SourceMixerHidden(obs_source_t *source)
@@ -3194,8 +3459,9 @@ bool OBSBasic::QueryRemoveSource(obs_source_t *source)
QMessageBox remove_source(this);
remove_source.setText(text);
- QAbstractButton *Yes =
+ QPushButton *Yes =
remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole);
+ remove_source.setDefaultButton(Yes);
remove_source.addButton(QTStr("No"), QMessageBox::NoRole);
remove_source.setIcon(QMessageBox::Question);
remove_source.setWindowTitle(QTStr("ConfirmRemove.Title"));
@@ -3213,6 +3479,8 @@ void trigger_sparkle_update();
void OBSBasic::TimedCheckForUpdates()
{
+ if (App()->IsUpdaterDisabled())
+ return;
if (!config_get_bool(App()->GlobalConfig(), "General",
"EnableAutoUpdates"))
return;
@@ -3391,31 +3659,6 @@ void OBSBasic::SceneItemAdded(void *data, calldata_t *params)
Q_ARG(OBSSceneItem, OBSSceneItem(item)));
}
-void OBSBasic::SceneItemSelected(void *data, calldata_t *params)
-{
- OBSBasic *window = static_cast(data);
-
- obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene");
- obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item");
-
- QMetaObject::invokeMethod(window, "SelectSceneItem",
- Q_ARG(OBSScene, scene),
- Q_ARG(OBSSceneItem, item), Q_ARG(bool, true));
-}
-
-void OBSBasic::SceneItemDeselected(void *data, calldata_t *params)
-{
- OBSBasic *window = static_cast(data);
-
- obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene");
- obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item");
-
- QMetaObject::invokeMethod(window, "SelectSceneItem",
- Q_ARG(OBSScene, scene),
- Q_ARG(OBSSceneItem, item),
- Q_ARG(bool, false));
-}
-
void OBSBasic::SourceCreated(void *data, calldata_t *params)
{
obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
@@ -3720,8 +3963,11 @@ int OBSBasic::ResetVideo()
ovi.output_height =
(uint32_t)config_get_uint(basicConfig, "Video", "OutputCY");
ovi.output_format = GetVideoFormatFromName(colorFormat);
- ovi.colorspace = astrcmpi(colorSpace, "601") == 0 ? VIDEO_CS_601
- : VIDEO_CS_709;
+ ovi.colorspace = astrcmpi(colorSpace, "601") == 0
+ ? VIDEO_CS_601
+ : (astrcmpi(colorSpace, "709") == 0
+ ? VIDEO_CS_709
+ : VIDEO_CS_SRGB);
ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL
: VIDEO_RANGE_PARTIAL;
ovi.adapter =
@@ -3729,14 +3975,14 @@ int OBSBasic::ResetVideo()
ovi.gpu_conversion = true;
ovi.scale_type = GetScaleType(basicConfig);
- if (ovi.base_width == 0 || ovi.base_height == 0) {
+ if (ovi.base_width < 8 || ovi.base_height < 8) {
ovi.base_width = 1920;
ovi.base_height = 1080;
config_set_uint(basicConfig, "Video", "BaseCX", 1920);
config_set_uint(basicConfig, "Video", "BaseCY", 1080);
}
- if (ovi.output_width == 0 || ovi.output_height == 0) {
+ if (ovi.output_width < 8 || ovi.output_height < 8) {
ovi.output_width = ovi.base_width;
ovi.output_height = ovi.base_height;
config_set_uint(basicConfig, "Video", "OutputCX",
@@ -3915,6 +4161,16 @@ void OBSBasic::EnumDialogs()
}
}
+void OBSBasic::ClearProjectors()
+{
+ for (size_t i = 0; i < projectors.size(); i++) {
+ if (projectors[i])
+ delete projectors[i];
+ }
+
+ projectors.clear();
+}
+
void OBSBasic::ClearSceneData()
{
disableSaving++;
@@ -3927,19 +4183,11 @@ void OBSBasic::ClearSceneData()
ClearQuickTransitions();
ui->transitions->clear();
- for (size_t i = 0; i < projectors.size(); i++) {
- if (projectors[i])
- delete projectors[i];
- }
+ ClearProjectors();
- projectors.clear();
+ for (int i = 0; i < MAX_CHANNELS; i++)
+ obs_set_output_source(i, nullptr);
- obs_set_output_source(0, nullptr);
- obs_set_output_source(1, nullptr);
- obs_set_output_source(2, nullptr);
- obs_set_output_source(3, nullptr);
- obs_set_output_source(4, nullptr);
- obs_set_output_source(5, nullptr);
lastScene = nullptr;
swapScene = nullptr;
programScene = nullptr;
@@ -4008,6 +4256,12 @@ void OBSBasic::closeEvent(QCloseEvent *event)
updateCheckThread->wait();
if (logUploadThread)
logUploadThread->wait();
+ if (devicePropertiesThread && devicePropertiesThread->isRunning()) {
+ devicePropertiesThread->wait();
+ devicePropertiesThread.reset();
+ }
+
+ QApplication::sendPostedEvents(this);
signalHandlers.clear();
@@ -4186,11 +4440,14 @@ void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current,
if (api)
api->on_event(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED);
+ UpdateContextBar();
+
UNUSED_PARAMETER(prev);
}
void OBSBasic::EditSceneName()
{
+ ui->scenesDock->removeAction(renameScene);
QListWidgetItem *item = ui->scenes->currentItem();
Qt::ItemFlags flags = item->flags();
@@ -4294,6 +4551,8 @@ void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos)
QTStr("SceneWindow"), this, SLOT(OpenSceneWindow()));
popup.addAction(sceneWindow);
+ popup.addAction(QTStr("Screenshot.Scene"), this,
+ SLOT(ScreenshotScene()));
popup.addSeparator();
popup.addAction(QTStr("Filters"), this,
SLOT(OpenSceneFilters()));
@@ -4645,6 +4904,9 @@ void OBSBasic::CreateSourcePopupMenu(int idx, bool preview)
popup.addAction(previewWindow);
+ popup.addAction(QTStr("Screenshot.Preview"), this,
+ SLOT(ScreenshotScene()));
+
popup.addSeparator();
}
@@ -4750,7 +5012,7 @@ void OBSBasic::CreateSourcePopupMenu(int idx, bool preview)
resizeOutput->setEnabled(!obs_video_active());
- if (width == 0 || height == 0)
+ if (width < 8 || height < 8)
resizeOutput->setEnabled(false);
scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering"));
@@ -4760,6 +5022,8 @@ void OBSBasic::CreateSourcePopupMenu(int idx, bool preview)
popup.addMenu(sourceProjector);
popup.addAction(sourceWindow);
+ popup.addAction(QTStr("Screenshot.Source"), this,
+ SLOT(ScreenshotSelectedSource()));
popup.addSeparator();
action = popup.addAction(QTStr("Interact"), this,
@@ -4957,8 +5221,9 @@ void OBSBasic::on_actionRemoveSource_triggered()
QMessageBox remove_items(this);
remove_items.setText(text);
- QAbstractButton *Yes = remove_items.addButton(
- QTStr("Yes"), QMessageBox::YesRole);
+ QPushButton *Yes = remove_items.addButton(QTStr("Yes"),
+ QMessageBox::YesRole);
+ remove_items.setDefaultButton(Yes);
remove_items.addButton(QTStr("No"), QMessageBox::NoRole);
remove_items.setIcon(QMessageBox::Question);
remove_items.setWindowTitle(QTStr("ConfirmRemove.Title"));
@@ -5052,7 +5317,7 @@ static BPtr ReadLogFile(const char *subdir, const char *log)
return file;
}
-void OBSBasic::UploadLog(const char *subdir, const char *file)
+void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash)
{
BPtr fileString{ReadLogFile(subdir, file)};
@@ -5063,6 +5328,9 @@ void OBSBasic::UploadLog(const char *subdir, const char *file)
return;
ui->menuLogFiles->setEnabled(false);
+#if defined(_WIN32) || defined(__APPLE__)
+ ui->menuCrashLogs->setEnabled(false);
+#endif
stringstream ss;
ss << "OBS " << App()->GetVersionString() << " log file uploaded at "
@@ -5078,8 +5346,13 @@ void OBSBasic::UploadLog(const char *subdir, const char *file)
"text/plain", ss.str().c_str());
logUploadThread.reset(thread);
- connect(thread, &RemoteTextThread::Result, this,
- &OBSBasic::logUploadFinished);
+ if (crash) {
+ connect(thread, &RemoteTextThread::Result, this,
+ &OBSBasic::crashUploadFinished);
+ } else {
+ connect(thread, &RemoteTextThread::Result, this,
+ &OBSBasic::logUploadFinished);
+ }
logUploadThread->start();
}
@@ -5095,28 +5368,28 @@ void OBSBasic::on_actionShowLogs_triggered()
void OBSBasic::on_actionUploadCurrentLog_triggered()
{
- UploadLog(CONFIG_DIR_NAME "/logs", App()->GetCurrentLog());
+ UploadLog(CONFIG_DIR_NAME "/logs", App()->GetCurrentLog(), false);
}
void OBSBasic::on_actionUploadLastLog_triggered()
{
- UploadLog(CONFIG_DIR_NAME "/logs", App()->GetLastLog());
+ UploadLog(CONFIG_DIR_NAME "/logs", App()->GetLastLog(), false);
}
void OBSBasic::on_actionViewCurrentLog_triggered()
{
- char logDir[512];
- if (GetConfigPath(logDir, sizeof(logDir), CONFIG_DIR_NAME "/logs") <= 0)
- return;
-
- const char *log = App()->GetCurrentLog();
+ if (!logView)
+ logView = new OBSLogViewer();
- string path = logDir;
- path += "/";
- path += log;
-
- QUrl url = QUrl::fromLocalFile(QT_UTF8(path.c_str()));
- QDesktopServices::openUrl(url);
+ if (!logView->isVisible()) {
+ logView->setVisible(true);
+ } else {
+ logView->setWindowState(
+ (logView->windowState() & ~Qt::WindowMinimized) |
+ Qt::WindowActive);
+ logView->activateWindow();
+ logView->raise();
+ }
}
void OBSBasic::on_actionShowCrashLogs_triggered()
@@ -5131,7 +5404,7 @@ void OBSBasic::on_actionShowCrashLogs_triggered()
void OBSBasic::on_actionUploadLastCrashLog_triggered()
{
- UploadLog(CONFIG_DIR_NAME "/crashes", App()->GetLastCrashLog());
+ UploadLog(CONFIG_DIR_NAME "/crashes", App()->GetLastCrashLog(), true);
}
void OBSBasic::on_actionCheckForUpdates_triggered()
@@ -5142,6 +5415,9 @@ void OBSBasic::on_actionCheckForUpdates_triggered()
void OBSBasic::logUploadFinished(const QString &text, const QString &error)
{
ui->menuLogFiles->setEnabled(true);
+#if defined(_WIN32) || defined(__APPLE__)
+ ui->menuCrashLogs->setEnabled(true);
+#endif
if (text.isEmpty()) {
OBSMessageBox::critical(
@@ -5149,13 +5425,34 @@ void OBSBasic::logUploadFinished(const QString &text, const QString &error)
error);
return;
}
+ openLogDialog(text, false);
+}
+
+void OBSBasic::crashUploadFinished(const QString &text, const QString &error)
+{
+ ui->menuLogFiles->setEnabled(true);
+#if defined(_WIN32) || defined(__APPLE__)
+ ui->menuCrashLogs->setEnabled(true);
+#endif
+
+ if (text.isEmpty()) {
+ OBSMessageBox::critical(
+ this, QTStr("LogReturnDialog.ErrorUploadingLog"),
+ error);
+ return;
+ }
+ openLogDialog(text, true);
+}
+
+void OBSBasic::openLogDialog(const QString &text, const bool crash)
+{
obs_data_t *returnData = obs_data_create_from_json(QT_TO_UTF8(text));
string resURL = obs_data_get_string(returnData, "url");
QString logURL = resURL.c_str();
obs_data_release(returnData);
- OBSLogReply logDialog(this, logURL);
+ OBSLogReply logDialog(this, logURL, crash);
logDialog.exec();
}
@@ -5202,6 +5499,8 @@ void OBSBasic::SceneNameEdited(QWidget *editor,
obs_source_t *source = obs_scene_get_source(scene);
RenameListItem(this, ui->scenes, source, text);
+ ui->scenesDock->addAction(renameScene);
+
if (api)
api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
@@ -5236,6 +5535,27 @@ void OBSBasic::OpenSceneFilters()
"==== Streaming Start ==============================================="
#define STREAMING_STOP \
"==== Streaming Stop ================================================"
+#define VIRTUAL_CAM_START \
+ "==== Virtual Camera Start =========================================="
+#define VIRTUAL_CAM_STOP \
+ "==== Virtual Camera Stop ==========================================="
+
+void OBSBasic::DisplayStreamStartError()
+{
+ QString message = !outputHandler->lastError.empty()
+ ? QTStr(outputHandler->lastError.c_str())
+ : QTStr("Output.StartFailedGeneric");
+ ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
+ ui->streamButton->setEnabled(true);
+ ui->streamButton->setChecked(false);
+
+ if (sysTrayStream) {
+ sysTrayStream->setText(ui->streamButton->text());
+ sysTrayStream->setEnabled(true);
+ }
+
+ QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message);
+}
void OBSBasic::StartStreaming()
{
@@ -5244,12 +5564,18 @@ void OBSBasic::StartStreaming()
if (disableOutputsRef)
return;
+ if (!outputHandler->SetupStreaming(service)) {
+ DisplayStreamStartError();
+ return;
+ }
+
if (api)
api->on_event(OBS_FRONTEND_EVENT_STREAMING_STARTING);
SaveProject();
ui->streamButton->setEnabled(false);
+ ui->streamButton->setChecked(false);
ui->streamButton->setText(QTStr("Basic.Main.Connecting"));
if (sysTrayStream) {
@@ -5258,21 +5584,7 @@ void OBSBasic::StartStreaming()
}
if (!outputHandler->StartStreaming(service)) {
- QString message =
- !outputHandler->lastError.empty()
- ? QTStr(outputHandler->lastError.c_str())
- : QTStr("Output.StartFailedGeneric");
- ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
- ui->streamButton->setEnabled(true);
- ui->streamButton->setChecked(false);
-
- if (sysTrayStream) {
- sysTrayStream->setText(ui->streamButton->text());
- sysTrayStream->setEnabled(true);
- }
-
- QMessageBox::critical(this, QTStr("Output.StartStreamFailed"),
- message);
+ DisplayStreamStartError();
return;
}
@@ -5330,7 +5642,7 @@ inline void OBSBasic::OnActivate()
App()->IncrementSleepInhibition();
UpdateProcessPriority();
- if (trayIcon)
+ if (trayIcon && trayIcon->isVisible())
trayIcon->setIcon(QIcon::fromTheme(
"obs-tray-active",
QIcon(":/res/images/rpan-tray_active.png")));
@@ -5348,15 +5660,19 @@ inline void OBSBasic::OnDeactivate()
App()->DecrementSleepInhibition();
ClearProcessPriority();
- if (trayIcon)
+ if (trayIcon && trayIcon->isVisible())
trayIcon->setIcon(QIcon::fromTheme(
"obs-tray", QIcon(":/res/images/rpan-studio.png")));
- } else if (trayIcon) {
+ } else if (outputHandler->Active() && trayIcon &&
+ trayIcon->isVisible()) {
if (os_atomic_load_bool(&recording_paused))
- trayIcon->setIcon(QIcon(":/res/images/rpan-studio_paused.png"));
+ trayIcon->setIcon(QIcon::fromTheme(
+ "obs-tray-paused",
+ QIcon(":/res/images/rpan-studio_paused.png")));
else
- trayIcon->setIcon(
- QIcon(":/res/images/rpan-tray_active.png"));
+ trayIcon->setIcon(QIcon::fromTheme(
+ "obs-tray-active",
+ QIcon(":/res/images/rpan-tray_active.png")));
}
}
@@ -5584,43 +5900,25 @@ void OBSBasic::StreamingStop(int code, QString last_error)
void OBSBasic::AutoRemux()
{
- const char *mode = config_get_string(basicConfig, "Output", "Mode");
- bool advanced = astrcmpi(mode, "Advanced") == 0;
-
- const char *path = !advanced ? config_get_string(basicConfig,
- "SimpleOutput",
- "FilePath")
- : config_get_string(basicConfig, "AdvOut",
- "RecFilePath");
-
- /* do not save if using FFmpeg output in advanced output mode */
- if (advanced) {
- const char *type =
- config_get_string(basicConfig, "AdvOut", "RecType");
- if (astrcmpi(type, "FFmpeg") == 0) {
- return;
- }
- }
-
- QString input;
- input += path;
- input += "/";
- input += remuxFilename.c_str();
+ QString input = outputHandler->lastRecordingPath.c_str();
+ if (input.isEmpty())
+ return;
- QFileInfo fi(remuxFilename.c_str());
+ QFileInfo fi(input);
+ QString suffix = fi.suffix();
/* do not remux if lossless */
- if (fi.suffix().compare("avi", Qt::CaseInsensitive) == 0) {
+ if (suffix.compare("avi", Qt::CaseInsensitive) == 0) {
return;
}
- QString output;
- output += path;
- output += "/";
- output += fi.completeBaseName();
- output += ".mp4";
+ QString path = fi.path();
- OBSRemux *remux = new OBSRemux(path, this, true);
+ QString output = input;
+ output.resize(output.size() - suffix.size());
+ output += "mp4";
+
+ OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true);
remux->show();
remux->AutoRemux(input, output);
}
@@ -5758,8 +6056,7 @@ void OBSBasic::RecordingStop(int code, QString last_error)
if (diskFullTimer->isActive())
diskFullTimer->stop();
- if (remuxAfterRecord)
- AutoRemux();
+ AutoRemux();
OnDeactivate();
UpdatePause(false);
@@ -5937,6 +6234,61 @@ void OBSBasic::ReplayBufferStop(int code)
UpdateReplayBuffer(false);
}
+void OBSBasic::StartVirtualCam()
+{
+ if (!outputHandler || !outputHandler->virtualCam)
+ return;
+ if (outputHandler->VirtualCamActive())
+ return;
+ if (disableOutputsRef)
+ return;
+
+ SaveProject();
+
+ if (!outputHandler->StartVirtualCam()) {
+ vcamButton->setChecked(false);
+ }
+}
+
+void OBSBasic::StopVirtualCam()
+{
+ if (!outputHandler || !outputHandler->virtualCam)
+ return;
+
+ SaveProject();
+
+ if (outputHandler->VirtualCamActive())
+ outputHandler->StopVirtualCam();
+
+ OnDeactivate();
+}
+
+void OBSBasic::OnVirtualCamStart()
+{
+ if (!outputHandler || !outputHandler->virtualCam)
+ return;
+
+ vcamButton->setText(QTStr("Basic.Main.StopVirtualCam"));
+ vcamButton->setChecked(true);
+
+ OnActivate();
+
+ blog(LOG_INFO, VIRTUAL_CAM_START);
+}
+
+void OBSBasic::OnVirtualCamStop(int)
+{
+ if (!outputHandler || !outputHandler->virtualCam)
+ return;
+
+ vcamButton->setText(QTStr("Basic.Main.StartVirtualCam"));
+ vcamButton->setChecked(false);
+
+ blog(LOG_INFO, VIRTUAL_CAM_STOP);
+
+ OnDeactivate();
+}
+
void OBSBasic::on_streamButton_clicked()
{
if (outputHandler->StreamingActive()) {
@@ -6066,6 +6418,20 @@ void OBSBasic::on_recordButton_clicked()
}
}
+void OBSBasic::VCamButtonClicked()
+{
+ if (outputHandler->VirtualCamActive()) {
+ StopVirtualCam();
+ } else {
+ if (!UIValidation::NoSourcesConfirmation(this)) {
+ vcamButton->setChecked(false);
+ return;
+ }
+
+ StartVirtualCam();
+ }
+}
+
void OBSBasic::on_settingsButton_clicked()
{
on_action_Settings_triggered();
@@ -6145,6 +6511,9 @@ void OBSBasic::on_program_customContextMenuRequested(const QPoint &)
popup.addAction(studioProgramWindow);
+ popup.addAction(QTStr("Screenshot.StudioProgram"), this,
+ SLOT(ScreenshotProgram()));
+
popup.exec(QCursor::pos());
}
@@ -6745,8 +7114,7 @@ OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor,
OBSProjector *projector =
new OBSProjector(nullptr, source, monitor, type);
- if (projector)
- projectors.emplace_back(projector);
+ projectors.emplace_back(projector);
return projector;
}
@@ -6872,6 +7240,10 @@ void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info)
Qt::LeftToRight, Qt::AlignCenter,
size(), rect));
}
+
+ if (info->alwaysOnTopOverridden)
+ projector->SetIsAlwaysOnTop(info->alwaysOnTop,
+ true);
}
}
}
@@ -6989,13 +7361,17 @@ void OBSBasic::on_resetUI_triggered()
resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical);
resizeDocks(docks, sizes, Qt::Horizontal);
#endif
+
+ activateWindow();
}
void OBSBasic::on_lockUI_toggled(bool lock)
{
QDockWidget::DockWidgetFeatures features =
lock ? QDockWidget::NoDockWidgetFeatures
- : QDockWidget::AllDockWidgetFeatures;
+ : (QDockWidget::DockWidgetClosable |
+ QDockWidget::DockWidgetMovable |
+ QDockWidget::DockWidgetFloatable);
QDockWidget::DockWidgetFeatures mainFeatures = features;
mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable;
@@ -7025,6 +7401,23 @@ void OBSBasic::on_toggleListboxToolbars_toggled(bool visible)
"ShowListboxToolbars", visible);
}
+void OBSBasic::ShowContextBar()
+{
+ on_toggleContextBar_toggled(true);
+}
+
+void OBSBasic::HideContextBar()
+{
+ on_toggleContextBar_toggled(false);
+}
+
+void OBSBasic::on_toggleContextBar_toggled(bool visible)
+{
+ config_set_bool(App()->GlobalConfig(), "BasicWindow",
+ "ShowContextToolbars", visible);
+ this->ui->contextContainer->setVisible(visible);
+}
+
void OBSBasic::on_toggleStatusBar_toggled(bool visible)
{
ui->statusbar->setVisible(visible);
@@ -7241,7 +7634,8 @@ void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason)
void OBSBasic::SysTrayNotify(const QString &text,
QSystemTrayIcon::MessageIcon n)
{
- if (trayIcon && QSystemTrayIcon::supportsMessages()) {
+ if (trayIcon && trayIcon->isVisible() &&
+ QSystemTrayIcon::supportsMessages()) {
QSystemTrayIcon::MessageIcon icon =
QSystemTrayIcon::MessageIcon(n);
trayIcon->showMessage("RPAN Studio", text, icon, 10000);
@@ -7263,12 +7657,11 @@ void OBSBasic::SystemTray(bool firstStarted)
if (firstStarted)
SystemTrayInit();
- if (!sysTrayWhenStarted && !sysTrayEnabled) {
+ if (!sysTrayEnabled) {
trayIcon->hide();
- } else if ((sysTrayWhenStarted && sysTrayEnabled) ||
- opt_minimize_tray) {
+ } else {
trayIcon->show();
- if (firstStarted) {
+ if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) {
QTimer::singleShot(50, this, SLOT(hide()));
EnablePreviewDisplay(false);
setVisible(false);
@@ -7277,12 +7670,6 @@ void OBSBasic::SystemTray(bool firstStarted)
#endif
opt_minimize_tray = false;
}
- } else if (sysTrayEnabled) {
- trayIcon->show();
- } else if (!sysTrayEnabled) {
- trayIcon->hide();
- } else if (!sysTrayWhenStarted && sysTrayEnabled) {
- trayIcon->hide();
}
if (isVisible())
@@ -7665,7 +8052,9 @@ QAction *OBSBasic::AddDockWidget(QDockWidget *dock)
bool lock = ui->lockUI->isChecked();
QDockWidget::DockWidgetFeatures features =
lock ? QDockWidget::NoDockWidgetFeatures
- : QDockWidget::AllDockWidgetFeatures;
+ : (QDockWidget::DockWidgetClosable |
+ QDockWidget::DockWidgetMovable |
+ QDockWidget::DockWidgetFloatable);
dock->setFeatures(features);
@@ -7757,8 +8146,10 @@ void OBSBasic::PauseRecording()
ui->statusbar->RecordingPaused();
- if (trayIcon)
- trayIcon->setIcon(QIcon(":/res/images/rpan-studio_paused.png"));
+ if (trayIcon && trayIcon->isVisible())
+ trayIcon->setIcon(QIcon::fromTheme(
+ "obs-tray-paused",
+ QIcon(":/res/images/rpan-studio_paused.png")));
os_atomic_set_bool(&recording_paused, true);
@@ -7786,9 +8177,10 @@ void OBSBasic::UnpauseRecording()
ui->statusbar->RecordingUnpaused();
- if (trayIcon)
- trayIcon->setIcon(
- QIcon(":/res/images/rpan-tray_active.png"));
+ if (trayIcon && trayIcon->isVisible())
+ trayIcon->setIcon(QIcon::fromTheme(
+ "obs-tray-active",
+ QIcon(":/res/images/rpan-tray_active.png")));
os_atomic_set_bool(&recording_paused, false);
@@ -7848,6 +8240,11 @@ void OBSBasic::UpdatePause(bool activate)
pause->setChecked(false);
pause->setProperty("themeID",
QVariant(QStringLiteral("pauseIconSmall")));
+
+ QSizePolicy sp;
+ sp.setHeightForWidth(true);
+ pause->setSizePolicy(sp);
+
connect(pause.data(), &QAbstractButton::clicked, this,
&OBSBasic::PauseToggled);
ui->recordingLayout->addWidget(pause.data());
@@ -7871,9 +8268,18 @@ void OBSBasic::UpdateReplayBuffer(bool activate)
replay->setChecked(false);
replay->setProperty("themeID",
QVariant(QStringLiteral("replayIconSmall")));
+
+ QSizePolicy sp;
+ sp.setHeightForWidth(true);
+ replay->setSizePolicy(sp);
+
connect(replay.data(), &QAbstractButton::clicked, this,
&OBSBasic::ReplayBufferSave);
replayLayout->addWidget(replay.data());
+ setTabOrder(replayLayout->itemAt(0)->widget(),
+ replayLayout->itemAt(1)->widget());
+ setTabOrder(replayLayout->itemAt(1)->widget(),
+ ui->buttonsVLayout->itemAt(3)->widget());
}
#define MBYTE (1024ULL * 1024ULL)
@@ -7913,6 +8319,18 @@ void OBSBasic::OutputPathInvalidMessage()
bool OBSBasic::OutputPathValid()
{
+ const char *mode = config_get_string(Config(), "Output", "Mode");
+ if (strcmp(mode, "Advanced") == 0) {
+ const char *advanced_mode =
+ config_get_string(Config(), "AdvOut", "RecType");
+ if (strcmp(advanced_mode, "FFmpeg") == 0) {
+ bool is_local = config_get_bool(Config(), "AdvOut",
+ "FFOutputToFile");
+ if (!is_local)
+ return true;
+ }
+ }
+
const char *path = GetCurrentOutputPath();
return path && *path && QDir(path).exists();
}
@@ -7951,15 +8369,8 @@ void OBSBasic::CheckDiskSpaceRemaining()
}
}
-void OBSBasic::ScenesReordered(const QModelIndex &parent, int start, int end,
- const QModelIndex &destination, int row)
+void OBSBasic::ScenesReordered()
{
- UNUSED_PARAMETER(parent);
- UNUSED_PARAMETER(start);
- UNUSED_PARAMETER(end);
- UNUSED_PARAMETER(destination);
- UNUSED_PARAMETER(row);
-
OBSProjector::UpdateMultiviewProjectors();
}
@@ -7981,6 +8392,37 @@ void OBSBasic::on_customContextMenuRequested(const QPoint &pos)
ui->viewMenuDocks->exec(mapToGlobal(pos));
}
+void OBSBasic::UpdateProjectorHideCursor()
+{
+ for (size_t i = 0; i < projectors.size(); i++)
+ projectors[i]->SetHideCursor();
+}
+
+void OBSBasic::UpdateProjectorAlwaysOnTop(bool top)
+{
+ for (size_t i = 0; i < projectors.size(); i++)
+ SetAlwaysOnTop(projectors[i], top);
+}
+
+void OBSBasic::ResetProjectors()
+{
+ obs_data_array_t *savedProjectorList = SaveProjectors();
+ ClearProjectors();
+ LoadSavedProjectors(savedProjectorList);
+ OpenSavedProjectors();
+ obs_data_array_release(savedProjectorList);
+}
+
+void OBSBasic::on_sourcePropertiesButton_clicked()
+{
+ on_actionSourceProperties_triggered();
+}
+
+void OBSBasic::on_sourceFiltersButton_clicked()
+{
+ OpenFilters();
+}
+
#if REDDIT_ENABLED
void OBSBasic::RedditOnReconnect(void *data, calldata_t *)
{
diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp
index a92baad..2fc2842 100644
--- a/UI/window-basic-main.hpp
+++ b/UI/window-basic-main.hpp
@@ -34,6 +34,7 @@
#include "window-projector.hpp"
#include "window-basic-about.hpp"
#include "auth-base.hpp"
+#include "log-viewer.hpp"
#include
@@ -78,6 +79,8 @@ struct SavedProjectorInfo {
int monitor;
std::string geometry;
std::string name;
+ bool alwaysOnTop;
+ bool alwaysOnTopOverridden;
};
struct QuickTransition {
@@ -160,6 +163,9 @@ class OBSBasic : public OBSMainWindow {
friend class ReplayBufferButton;
friend class ExtraBrowsersModel;
friend class ExtraBrowsersDelegate;
+ friend class DeviceCaptureToolbar;
+ friend class DeviceToolbarPropertiesThread;
+ friend struct BasicOutputHandler;
friend class RedditStartStreamDialog;
friend struct OBSStudioAPI;
@@ -195,6 +201,7 @@ class OBSBasic : public OBSMainWindow {
bool copyVisible = true;
bool closing = false;
+ QScopedPointer devicePropertiesThread;
QScopedPointer whatsNewInitThread;
QScopedPointer updateCheckThread;
QScopedPointer introCheckThread;
@@ -208,6 +215,8 @@ class OBSBasic : public OBSMainWindow {
QPointer statsDock;
QPointer about;
+ OBSLogViewer *logView = nullptr;
+
QPointer cpuUsageTimer;
QPointer diskFullTimer;
@@ -227,7 +236,6 @@ class OBSBasic : public OBSMainWindow {
gs_vertbuffer_t *circle = nullptr;
bool sceneChanging = false;
- bool ignoreSelectionUpdate = false;
int previewX = 0, previewY = 0;
int previewCX = 0, previewCY = 0;
@@ -251,6 +259,9 @@ class OBSBasic : public OBSMainWindow {
QScopedPointer pause;
QScopedPointer replay;
+ QPointer vcamButton;
+ bool vcamEnabled = false;
+
QScopedPointer trayIcon;
QPointer sysTrayStream;
QPointer sysTrayRecord;
@@ -272,6 +283,8 @@ class OBSBasic : public OBSMainWindow {
QPointer deinterlaceMenu;
QPointer perSceneTransitionMenu;
QPointer shortcutFilter;
+ QPointer renameScene;
+ QPointer renameSource;
QPointer programWidget;
QPointer programLayout;
@@ -293,7 +306,7 @@ class OBSBasic : public OBSMainWindow {
void UpdateVolumeControlsPeakMeterType();
void ClearVolumeControls();
- void UploadLog(const char *subdir, const char *file);
+ void UploadLog(const char *subdir, const char *file, const bool crash);
void Save(const char *file);
void Load(const char *file);
@@ -340,6 +353,7 @@ class OBSBasic : public OBSMainWindow {
void CloseDialogs();
void ClearSceneData();
+ void ClearProjectors();
void Nudge(int dist, MoveDir dir);
@@ -372,7 +386,8 @@ class OBSBasic : public OBSMainWindow {
QModelIndexList GetAllSelectedSourceItems();
obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys,
- replayBufHotkeys, togglePreviewHotkeys;
+ replayBufHotkeys, vcamHotkeys, togglePreviewHotkeys,
+ contextBarHotkeys;
obs_hotkey_id forceStreamingStopHotkey;
void InitDefaultTransitions();
@@ -383,9 +398,13 @@ class OBSBasic : public OBSMainWindow {
void LoadTransitions(obs_data_array_t *transitions);
obs_source_t *fadeTransition;
+ obs_source_t *cutTransition;
void CreateProgramDisplay();
void CreateProgramOptions();
+ int TransitionCount();
+ int AddTransitionBeforeSeparator(const QString &name,
+ obs_source_t *source);
void AddQuickTransitionId(int id);
void AddQuickTransition();
void AddQuickTransitionHotkey(QuickTransition *qt);
@@ -427,6 +446,8 @@ class OBSBasic : public OBSMainWindow {
obs_hotkey_id togglePreviewProgramHotkey = 0;
obs_hotkey_id transitionHotkey = 0;
obs_hotkey_id statsHotkey = 0;
+ obs_hotkey_id screenshotHotkey = 0;
+ obs_hotkey_id sourceScreenshotHotkey = 0;
int quickTransitionIdCounter = 1;
bool overridingTransition = false;
@@ -517,10 +538,18 @@ class OBSBasic : public OBSMainWindow {
OBSSource GetOverrideTransition(OBSSource source);
int GetOverrideTransitionDuration(OBSSource source);
+ void UpdateProjectorHideCursor();
+ void UpdateProjectorAlwaysOnTop(bool top);
+ void ResetProjectors();
+
+ QPointer screenshotData;
+
public slots:
void DeferSaveBegin();
void DeferSaveEnd();
+ void DisplayStreamStartError();
+
void StartStreaming();
void StopStreaming();
void ForceStopStreaming();
@@ -548,6 +577,12 @@ public slots:
void ReplayBufferStopping();
void ReplayBufferStop(int code);
+ void StartVirtualCam();
+ void StopVirtualCam();
+
+ void OnVirtualCamStart();
+ void OnVirtualCamStop(int code);
+
void SaveProjectDeferred();
void SaveProject();
@@ -565,6 +600,8 @@ public slots:
void UpdatePatronJson(const QString &text, const QString &error);
+ void ShowContextBar();
+ void HideContextBar();
void PauseRecording();
void UnpauseRecording();
@@ -574,8 +611,6 @@ private slots:
void RemoveScene(OBSSource source);
void RenameSources(OBSSource source, QString newName, QString prevName);
- void SelectSceneItem(OBSScene scene, OBSSceneItem item, bool select);
-
void ActivateAudioSource(OBSSource source);
void DeactivateAudioSource(OBSSource source);
@@ -590,7 +625,7 @@ private slots:
void ProcessHotkey(obs_hotkey_id id, bool pressed);
- void AddTransition();
+ void AddTransition(QString id);
void RenameTransition();
void TransitionClicked();
void TransitionStopped();
@@ -641,8 +676,7 @@ private slots:
void CheckDiskSpaceRemaining();
void OpenSavedProjector(SavedProjectorInfo *info);
- void ScenesReordered(const QModelIndex &parent, int start, int end,
- const QModelIndex &destination, int row);
+ void ScenesReordered();
void ResetStatsHotkey();
@@ -672,8 +706,6 @@ private slots:
static void SceneReordered(void *data, calldata_t *params);
static void SceneRefreshed(void *data, calldata_t *params);
static void SceneItemAdded(void *data, calldata_t *params);
- static void SceneItemSelected(void *data, calldata_t *params);
- static void SceneItemDeselected(void *data, calldata_t *params);
static void SourceCreated(void *data, calldata_t *params);
static void SourceRemoved(void *data, calldata_t *params);
static void SourceActivated(void *data, calldata_t *params);
@@ -727,6 +759,8 @@ private slots:
return os_atomic_load_bool(&previewProgramMode);
}
+ inline bool VCamEnabled() const { return vcamEnabled; }
+
bool StreamingActive() const;
bool Active() const;
@@ -734,6 +768,7 @@ private slots:
int ResetVideo();
bool ResetAudio();
+ void AddVCamButton();
void ResetOutputs();
void ResetAudioDevice(const char *sourceId, const char *deviceId,
@@ -874,7 +909,12 @@ private slots:
void on_streamButton_clicked();
void on_recordButton_clicked();
+ void VCamButtonClicked();
void on_settingsButton_clicked();
+ void Screenshot(OBSSource source_ = nullptr);
+ void ScreenshotSelectedSource();
+ void ScreenshotProgram();
+ void ScreenshotScene();
void on_actionHelpPortal_triggered();
void on_actionWebsite_triggered();
@@ -904,17 +944,21 @@ private slots:
void on_actionAlwaysOnTop_triggered();
void on_toggleListboxToolbars_toggled(bool visible);
+ void on_toggleContextBar_toggled(bool visible);
void on_toggleStatusBar_toggled(bool visible);
void on_toggleSourceIcons_toggled(bool visible);
void on_transitions_currentIndexChanged(int index);
- void on_transitionAdd_clicked();
void on_transitionRemove_clicked();
void on_transitionProps_clicked();
void on_transitionDuration_valueChanged(int value);
void on_modeSwitch_clicked();
+ // Source Context Buttons
+ void on_sourcePropertiesButton_clicked();
+ void on_sourceFiltersButton_clicked();
+
void on_autoConfigure_triggered();
void on_stats_triggered();
@@ -924,6 +968,8 @@ private slots:
void PauseToggled();
void logUploadFinished(const QString &text, const QString &error);
+ void crashUploadFinished(const QString &text, const QString &error);
+ void openLogDialog(const QString &text, const bool crash);
void updateCheckFinished();
@@ -974,6 +1020,9 @@ public slots:
bool RecordingActive();
bool ReplayBufferActive();
+ void ClearContextBar();
+ void UpdateContextBar();
+
public:
explicit OBSBasic(QWidget *parent = 0);
virtual ~OBSBasic();
diff --git a/UI/window-basic-preview.cpp b/UI/window-basic-preview.cpp
index 6b27849..a73604a 100644
--- a/UI/window-basic-preview.cpp
+++ b/UI/window-basic-preview.cpp
@@ -495,13 +495,15 @@ void OBSBasicPreview::keyReleaseEvent(QKeyEvent *event)
void OBSBasicPreview::wheelEvent(QWheelEvent *event)
{
- if (scrollMode && IsFixedScaling() &&
- event->orientation() == Qt::Vertical) {
- if (event->delta() > 0)
- SetScalingLevel(scalingLevel + 1);
- else if (event->delta() < 0)
- SetScalingLevel(scalingLevel - 1);
- emit DisplayResized();
+ if (scrollMode && IsFixedScaling()) {
+ const int delta = event->angleDelta().y();
+ if (delta != 0) {
+ if (delta > 0)
+ SetScalingLevel(scalingLevel + 1);
+ else
+ SetScalingLevel(scalingLevel - 1);
+ emit DisplayResized();
+ }
}
OBSQTDisplay::wheelEvent(event);
@@ -581,6 +583,25 @@ void OBSBasicPreview::mousePressEvent(QMouseEvent *event)
mousePos = startPos;
}
+void OBSBasicPreview::UpdateCursor(uint32_t &flags)
+{
+ if (!flags && cursor().shape() != Qt::OpenHandCursor)
+ unsetCursor();
+ if (cursor().shape() != Qt::ArrowCursor)
+ return;
+
+ if ((flags & ITEM_LEFT && flags & ITEM_TOP) ||
+ (flags & ITEM_RIGHT && flags & ITEM_BOTTOM))
+ setCursor(Qt::SizeFDiagCursor);
+ else if ((flags & ITEM_LEFT && flags & ITEM_BOTTOM) ||
+ (flags & ITEM_RIGHT && flags & ITEM_TOP))
+ setCursor(Qt::SizeBDiagCursor);
+ else if (flags & ITEM_LEFT || flags & ITEM_RIGHT)
+ setCursor(Qt::SizeHorCursor);
+ else if (flags & ITEM_TOP || flags & ITEM_BOTTOM)
+ setCursor(Qt::SizeVerCursor);
+}
+
static bool select_one(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
obs_sceneitem_t *selectedItem =
@@ -683,6 +704,7 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event)
mouseMoved = false;
cropping = false;
selectionBox = false;
+ unsetCursor();
OBSSceneItem item = GetItemAtPos(pos, true);
@@ -1088,6 +1110,9 @@ void OBSBasicPreview::BoxItems(const vec2 &startPos, const vec2 &pos)
if (!scene)
return;
+ if (cursor().shape() != Qt::CrossCursor)
+ setCursor(Qt::CrossCursor);
+
SceneFindBoxData data(startPos, pos);
obs_scene_enum_items(scene, FindItemsInBox, &data);
@@ -1421,6 +1446,8 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event)
if (locked)
return;
+ bool updateCursor = false;
+
if (mouseDown) {
vec2 pos = GetMouseEventPos(event);
@@ -1456,6 +1483,8 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event)
StretchItem(pos);
} else if (mouseOverItems) {
+ if (cursor().shape() != Qt::SizeAllCursor)
+ setCursor(Qt::SizeAllCursor);
selectionBox = false;
MoveItems(pos);
} else {
@@ -1474,6 +1503,27 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event)
std::lock_guard lock(selectMutex);
hoveredPreviewItems.clear();
hoveredPreviewItems.push_back(item);
+
+ if (!mouseMoved && hoveredPreviewItems.size() > 0) {
+ mousePos = pos;
+ OBSBasic *main = reinterpret_cast(
+ App()->GetMainWindow());
+#ifdef SUPPORTS_FRACTIONAL_SCALING
+ float scale = main->devicePixelRatioF();
+#else
+ float scale = main->devicePixelRatio();
+#endif
+ float x = float(event->x()) - main->previewX / scale;
+ float y = float(event->y()) - main->previewY / scale;
+ vec2_set(&startPos, x, y);
+ updateCursor = true;
+ }
+ }
+
+ if (updateCursor) {
+ GetStretchHandleData(startPos);
+ uint32_t stretchFlags = (uint32_t)stretchHandle;
+ UpdateCursor(stretchFlags);
}
}
diff --git a/UI/window-basic-preview.hpp b/UI/window-basic-preview.hpp
index 2473f62..fba3613 100644
--- a/UI/window-basic-preview.hpp
+++ b/UI/window-basic-preview.hpp
@@ -92,6 +92,8 @@ class OBSBasicPreview : public OBSQTDisplay {
void GetStretchHandleData(const vec2 &pos);
+ void UpdateCursor(uint32_t &flags);
+
void SnapStretchingToScreen(vec3 &tl, vec3 &br);
void ClampAspect(vec3 &tl, vec3 &br, vec2 &size, const vec2 &baseSize);
vec3 CalculateStretchPos(const vec3 &tl, const vec3 &br);
@@ -105,7 +107,8 @@ class OBSBasicPreview : public OBSQTDisplay {
void ProcessClick(const vec2 &pos);
public:
- OBSBasicPreview(QWidget *parent, Qt::WindowFlags flags = 0);
+ OBSBasicPreview(QWidget *parent,
+ Qt::WindowFlags flags = Qt::WindowFlags());
~OBSBasicPreview();
static OBSBasicPreview *Get();
diff --git a/UI/window-basic-properties.cpp b/UI/window-basic-properties.cpp
index 689534d..52dd19f 100644
--- a/UI/window-basic-properties.cpp
+++ b/UI/window-basic-properties.cpp
@@ -205,6 +205,7 @@ OBSBasicProperties::~OBSBasicProperties()
}
obs_source_dec_showing(source);
main->SaveProject();
+ main->UpdateContextBar();
}
void OBSBasicProperties::AddPreviewButton()
diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp
index 183e836..2c88471 100644
--- a/UI/window-basic-settings-stream.cpp
+++ b/UI/window-basic-settings-stream.cpp
@@ -45,8 +45,6 @@ void OBSBasicSettings::InitStreamPage()
ui->bandwidthTestEnable->setVisible(false);
ui->twitchAddonDropdown->setVisible(false);
ui->twitchAddonLabel->setVisible(false);
- ui->mixerAddonDropdown->setVisible(false);
- ui->mixerAddonLabel->setVisible(false);
ui->currentAccountLabel->setVisible(false);
ui->currentAccount->setVisible(false);
@@ -75,15 +73,14 @@ void OBSBasicSettings::InitStreamPage()
ui->twitchAddonDropdown->addItem(
QTStr("Basic.Settings.Stream.TTVAddon.Both"));
- ui->mixerAddonDropdown->addItem(
- QTStr("Basic.Settings.Stream.MixerAddon.None"));
- ui->mixerAddonDropdown->addItem(
- QTStr("Basic.Settings.Stream.MixerAddon.MEE"));
-
connect(ui->service, SIGNAL(currentIndexChanged(int)), this,
SLOT(UpdateServerList()));
connect(ui->service, SIGNAL(currentIndexChanged(int)), this,
SLOT(UpdateKeyLink()));
+ connect(ui->customServer, SIGNAL(textChanged(const QString &)), this,
+ SLOT(UpdateKeyLink()));
+ connect(ui->customServer, SIGNAL(editingFinished(const QString &)),
+ this, SLOT(UpdateKeyLink()));
#if REDDIT_ENABLED
ui->serverLabel->setVisible(false);
@@ -130,9 +127,6 @@ void OBSBasicSettings::LoadStream1Settings()
idx = config_get_int(main->Config(), "Twitch", "AddonChoice");
ui->twitchAddonDropdown->setCurrentIndex(idx);
-
- idx = config_get_int(main->Config(), "Mixer", "AddonChoice");
- ui->mixerAddonDropdown->setCurrentIndex(idx);
}
UpdateServerList();
@@ -194,9 +188,6 @@ void OBSBasicSettings::SaveStream1Settings()
}
}
- obs_data_set_bool(settings, "bwtest",
- ui->bandwidthTestEnable->isChecked());
-
if (!!auth && strcmp(auth->service(), "Twitch") == 0) {
bool choiceExists = config_has_user_value(
main->Config(), "Twitch", "AddonChoice");
@@ -209,19 +200,14 @@ void OBSBasicSettings::SaveStream1Settings()
if (choiceExists && currentChoice != newChoice)
forceAuthReload = true;
- }
- if (!!auth && strcmp(auth->service(), "Mixer") == 0) {
- bool choiceExists = config_has_user_value(
- main->Config(), "Mixer", "AddonChoice");
- int currentChoice =
- config_get_int(main->Config(), "Mixer", "AddonChoice");
- int newChoice = ui->mixerAddonDropdown->currentIndex();
- config_set_int(main->Config(), "Mixer", "AddonChoice",
- newChoice);
-
- if (choiceExists && currentChoice != newChoice)
- forceAuthReload = true;
+ obs_data_set_bool(settings, "bwtest",
+ ui->bandwidthTestEnable->isChecked());
+ } else {
+ obs_data_set_bool(settings, "bwtest", false);
+ }
+ if (!!auth && strncmp(auth->service(), "Reddit", 6) == 0) {
+ forceAuthReload = true;
}
if (!!auth && strncmp(auth->service(), "Reddit", 6) == 0) {
forceAuthReload = true;
@@ -245,12 +231,8 @@ void OBSBasicSettings::SaveStream1Settings()
void OBSBasicSettings::UpdateKeyLink()
{
- if (IsCustomService()) {
- ui->getStreamKeyButton->hide();
- return;
- }
-
QString serviceName = ui->service->currentText();
+ QString customServer = ui->customServer->text();
QString streamKeyLink;
if (serviceName == "Twitch") {
streamKeyLink =
@@ -260,12 +242,16 @@ void OBSBasicSettings::UpdateKeyLink()
} else if (serviceName.startsWith("Restream.io")) {
streamKeyLink =
"https://restream.io/settings/streaming-setup?from=OBS";
- } else if (serviceName == "Facebook Live") {
- streamKeyLink = "https://www.facebook.com/live/create?ref=OBS";
+ } else if (serviceName == "Facebook Live" ||
+ (customServer.contains("fbcdn.net") && IsCustomService())) {
+ streamKeyLink =
+ "https://www.facebook.com/live/producer?ref=OBS";
} else if (serviceName.startsWith("Twitter")) {
streamKeyLink = "https://www.pscp.tv/account/producer";
} else if (serviceName.startsWith("YouStreamer")) {
streamKeyLink = "https://app.youstreamer.com/stream/";
+ } else if (serviceName == "Trovo") {
+ streamKeyLink = "https://studio.trovo.live/mychannel/stream";
}
if (QString(streamKeyLink).isNull()) {
@@ -301,7 +287,7 @@ void OBSBasicSettings::LoadServices(bool showAll)
}
if (showAll)
- names.sort();
+ names.sort(Qt::CaseInsensitive);
for (QString &name : names)
ui->service->addItem(name);
@@ -348,8 +334,6 @@ void OBSBasicSettings::on_service_currentIndexChanged(int)
ui->bandwidthTestEnable->setVisible(false);
ui->twitchAddonDropdown->setVisible(false);
ui->twitchAddonLabel->setVisible(false);
- ui->mixerAddonDropdown->setVisible(false);
- ui->mixerAddonLabel->setVisible(false);
#ifdef BROWSER_AVAILABLE
if (cef) {
@@ -511,10 +495,15 @@ void OBSBasicSettings::OnOAuthStreamKeyConnected()
ui->bandwidthTestEnable->setVisible(true);
ui->twitchAddonLabel->setVisible(true);
ui->twitchAddonDropdown->setVisible(true);
+ } else {
+ ui->bandwidthTestEnable->setChecked(false);
}
- if (strcmp(a->service(), "Mixer") == 0) {
- ui->mixerAddonLabel->setVisible(true);
- ui->mixerAddonDropdown->setVisible(true);
+ if (strncmp(a->service(), "Reddit", 6) == 0) {
+ auto redditAuth = static_cast(a);
+ ui->currentAccount->setVisible(true);
+ ui->currentAccountLabel->setVisible(true);
+ ui->currentAccount->setText(redditAuth->GetUsername()->c_str());
+ ui->useStreamKey->setVisible(false);
}
if (strncmp(a->service(), "Reddit", 6) == 0) {
auto redditAuth = static_cast(a);
@@ -581,6 +570,8 @@ void OBSBasicSettings::on_disconnectAccount_clicked()
OAuth::DeleteCookies(service);
#endif
+ ui->bandwidthTestEnable->setChecked(false);
+
ui->streamKeyWidget->setVisible(true);
ui->streamKeyLabel->setVisible(true);
ui->connectAccount2->setVisible(true);
diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp
index 6bf0449..694790e 100644
--- a/UI/window-basic-settings.cpp
+++ b/UI/window-basic-settings.cpp
@@ -27,7 +27,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -55,6 +54,35 @@
using namespace std;
+class SettingsEventFilter : public QObject {
+ QScopedPointer shortcutFilter;
+
+public:
+ inline SettingsEventFilter()
+ : shortcutFilter((OBSEventFilter *)CreateShortcutFilter())
+ {
+ }
+
+protected:
+ bool eventFilter(QObject *obj, QEvent *event) override
+ {
+ int key;
+
+ switch (event->type()) {
+ case QEvent::KeyPress:
+ case QEvent::KeyRelease:
+ key = static_cast(event)->key();
+ if (key == Qt::Key_Escape) {
+ return false;
+ }
+ default:
+ break;
+ }
+
+ return shortcutFilter->filter(obj, event);
+ }
+};
+
// Used for QVariant in codec comboboxes
namespace {
static bool StringEquals(QString left, QString right)
@@ -103,6 +131,11 @@ static inline bool ResTooHigh(uint32_t cx, uint32_t cy)
return cx > 16384 || cy > 16384;
}
+static inline bool ResTooLow(uint32_t cx, uint32_t cy)
+{
+ return cx < 8 || cy < 8;
+}
+
/* parses "[width]x[height]", string, i.e. 1024x768 */
static bool ConvertResText(const char *res, uint32_t &cx, uint32_t &cy)
{
@@ -137,7 +170,7 @@ static bool ConvertResText(const char *res, uint32_t &cx, uint32_t &cy)
if (lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE))
return false;
- if (ResTooHigh(cx, cy)) {
+ if (ResTooHigh(cx, cy) || ResTooLow(cx, cy)) {
cx = cy = 0;
return false;
}
@@ -279,6 +312,24 @@ static std::tuple aspect_ratio(int cx, int cy)
return std::make_tuple(newCX, newCY);
}
+static inline void HighlightGroupBoxLabel(QGroupBox *gb, QWidget *widget,
+ QString objectName)
+{
+ QFormLayout *layout = qobject_cast(gb->layout());
+
+ if (!layout)
+ return;
+
+ QLabel *label = qobject_cast(layout->labelForField(widget));
+
+ if (label) {
+ label->setObjectName(objectName);
+
+ label->style()->unpolish(label);
+ label->style()->polish(label);
+ }
+}
+
void RestrictResetBitrates(initializer_list boxes, int maxbitrate);
void OBSBasicSettings::HookWidget(QWidget *widget, const char *signal,
@@ -322,6 +373,8 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
ui->setupUi(this);
+ ui->settingsPages->setCurrentIndex(0);
+
main->EnableOutputs(false);
PopulateAACBitrates({ui->simpleOutputABitrate, ui->advOutTrack1Bitrate,
@@ -371,7 +424,6 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
HookWidget(ui->key, EDIT_CHANGED, STREAM1_CHANGED);
HookWidget(ui->bandwidthTestEnable, CHECK_CHANGED, STREAM1_CHANGED);
HookWidget(ui->twitchAddonDropdown, COMBO_CHANGED, STREAM1_CHANGED);
- HookWidget(ui->mixerAddonDropdown, COMBO_CHANGED, STREAM1_CHANGED);
HookWidget(ui->useAuth, CHECK_CHANGED, STREAM1_CHANGED);
HookWidget(ui->authUsername, EDIT_CHANGED, STREAM1_CHANGED);
HookWidget(ui->authPw, EDIT_CHANGED, STREAM1_CHANGED);
@@ -508,6 +560,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
HookWidget(ui->hotkeyFocusType, COMBO_CHANGED, ADV_CHANGED);
HookWidget(ui->autoRemux, CHECK_CHANGED, ADV_CHANGED);
HookWidget(ui->dynBitrate, CHECK_CHANGED, ADV_CHANGED);
+ HookWidget(ui->redditAmaModeCheckBox, CHECK_CHANGED, ADV_CHANGED);
/* clang-format on */
#define ADD_HOTKEY_FOCUS_TYPE(s) \
@@ -633,7 +686,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
// Initialize libff library
ff_init();
- installEventFilter(CreateShortcutFilter());
+ installEventFilter(new SettingsEventFilter());
LoadEncoderTypes();
LoadColorRanges();
@@ -2101,6 +2154,8 @@ void OBSBasicSettings::LoadListValues(QComboBox *widget, obs_property_t *prop,
"UnknownAudioDevice"),
var);
widget->setCurrentIndex(0);
+ HighlightGroupBoxLabel(ui->audioDevicesGroupBox, widget,
+ "errorLabel");
}
}
@@ -2373,6 +2428,7 @@ void OBSBasicSettings::LoadAdvancedSettings()
App()->GlobalConfig(), "General", "HotkeyFocusType");
bool dynBitrate =
config_get_bool(main->Config(), "Output", "DynamicBitrate");
+ bool redditAmaMode = config_get_bool(main->Config(), "Reddit", "AMAModeEnabled");
loading = true;
@@ -2401,6 +2457,7 @@ void OBSBasicSettings::LoadAdvancedSettings()
ui->streamDelayEnable->setChecked(enableDelay);
ui->autoRemux->setChecked(autoRemux);
ui->dynBitrate->setChecked(dynBitrate);
+ ui->redditAmaModeCheckBox->setChecked(redditAmaMode);
SetComboByName(ui->colorFormat, videoColorFormat);
SetComboByName(ui->colorSpace, videoColorSpace);
@@ -2688,7 +2745,7 @@ void OBSBasicSettings::LoadHotkeySettings(obs_hotkey_id ignoreKey)
if (obs_scene_from_source(source))
scenes.emplace_back(source, label, hw);
- else
+ else if (obs_source_get_name(source) != NULL)
sources.emplace_back(source, label, hw);
return false;
@@ -2900,11 +2957,24 @@ void OBSBasicSettings::SaveGeneralSettings()
"WarnBeforeStoppingRecord",
ui->warnBeforeRecordStop->isChecked());
- config_set_bool(GetGlobalConfig(), "BasicWindow", "HideProjectorCursor",
- ui->hideProjectorCursor->isChecked());
- config_set_bool(GetGlobalConfig(), "BasicWindow",
- "ProjectorAlwaysOnTop",
+ if (WidgetChanged(ui->hideProjectorCursor)) {
+ config_set_bool(GetGlobalConfig(), "BasicWindow",
+ "HideProjectorCursor",
+ ui->hideProjectorCursor->isChecked());
+ main->UpdateProjectorHideCursor();
+ }
+
+ if (WidgetChanged(ui->projectorAlwaysOnTop)) {
+ config_set_bool(GetGlobalConfig(), "BasicWindow",
+ "ProjectorAlwaysOnTop",
+ ui->projectorAlwaysOnTop->isChecked());
+#if defined(_WIN32) || defined(__APPLE__)
+ main->UpdateProjectorAlwaysOnTop(
ui->projectorAlwaysOnTop->isChecked());
+#else
+ main->ResetProjectors();
+#endif
+ }
if (WidgetChanged(ui->recordWhenStreaming))
config_set_bool(GetGlobalConfig(), "BasicWindow",
@@ -3105,6 +3175,7 @@ void OBSBasicSettings::SaveAdvancedSettings()
SaveComboData(ui->bindToIP, "Output", "BindIP");
SaveCheckBox(ui->autoRemux, "Video", "AutoRemux");
SaveCheckBox(ui->dynBitrate, "Output", "DynamicBitrate");
+ SaveCheckBox(ui->redditAmaModeCheckBox, "Reddit", "AMAModeEnabled");
#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO
QString newDevice = ui->monitoringDevice->currentData().toString();
@@ -3565,6 +3636,9 @@ bool OBSBasicSettings::QueryChanges()
} else if (button == QMessageBox::Yes) {
SaveSettings();
} else {
+ if (savedTheme != App()->GetTheme())
+ App()->SetTheme(savedTheme);
+
LoadSettings(true);
#ifdef _WIN32
if (toggleAero)
@@ -3579,14 +3653,14 @@ bool OBSBasicSettings::QueryChanges()
void OBSBasicSettings::closeEvent(QCloseEvent *event)
{
- if (Changed() && !QueryChanges())
+ if (!AskIfCanCloseSettings())
event->ignore();
+}
- if (forceAuthReload) {
- main->auth->Save();
- main->auth->Load();
- forceAuthReload = false;
- }
+void OBSBasicSettings::reject()
+{
+ if (AskIfCanCloseSettings())
+ close();
}
void OBSBasicSettings::on_theme_activated(int idx)
@@ -3641,10 +3715,9 @@ void OBSBasicSettings::on_buttonBox_clicked(QAbstractButton *button)
void OBSBasicSettings::on_simpleOutputBrowse_clicked()
{
- QString dir = QFileDialog::getExistingDirectory(
+ QString dir = SelectDirectory(
this, QTStr("Basic.Settings.Output.SelectDirectory"),
- ui->simpleOutputPath->text(),
- QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
+ ui->simpleOutputPath->text());
if (dir.isEmpty())
return;
@@ -3653,10 +3726,9 @@ void OBSBasicSettings::on_simpleOutputBrowse_clicked()
void OBSBasicSettings::on_advOutRecPathBrowse_clicked()
{
- QString dir = QFileDialog::getExistingDirectory(
+ QString dir = SelectDirectory(
this, QTStr("Basic.Settings.Output.SelectDirectory"),
- ui->advOutRecPath->text(),
- QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
+ ui->advOutRecPath->text());
if (dir.isEmpty())
return;
@@ -3665,10 +3737,9 @@ void OBSBasicSettings::on_advOutRecPathBrowse_clicked()
void OBSBasicSettings::on_advOutFFPathBrowse_clicked()
{
- QString dir = QFileDialog::getExistingDirectory(
+ QString dir = SelectDirectory(
this, QTStr("Basic.Settings.Output.SelectDirectory"),
- ui->advOutRecPath->text(),
- QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
+ ui->advOutRecPath->text());
if (dir.isEmpty())
return;
@@ -3840,6 +3911,22 @@ void OBSBasicSettings::RecalcOutputResPixels(const char *resText)
}
}
+bool OBSBasicSettings::AskIfCanCloseSettings()
+{
+ bool canCloseSettings = false;
+
+ if (!Changed() || QueryChanges())
+ canCloseSettings = true;
+
+ if (forceAuthReload) {
+ main->auth->Save();
+ main->auth->Load();
+ forceAuthReload = false;
+ }
+
+ return canCloseSettings;
+}
+
void OBSBasicSettings::on_filenameFormatting_textEdited(const QString &text)
{
#ifdef __APPLE__
diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp
index bd1a183..51179a2 100644
--- a/UI/window-basic-settings.hpp
+++ b/UI/window-basic-settings.hpp
@@ -289,6 +289,8 @@ private slots:
void RecalcOutputResPixels(const char *resText);
+ bool AskIfCanCloseSettings();
+
QIcon generalIcon;
QIcon streamIcon;
QIcon outputIcon;
@@ -375,7 +377,8 @@ private slots:
void SetAdvancedIcon(const QIcon &icon);
protected:
- virtual void closeEvent(QCloseEvent *event);
+ virtual void closeEvent(QCloseEvent *event) override;
+ void reject() override;
public:
OBSBasicSettings(QWidget *parent);
diff --git a/UI/window-basic-stats.cpp b/UI/window-basic-stats.cpp
index 1c68dcb..0bf39f5 100644
--- a/UI/window-basic-stats.cpp
+++ b/UI/window-basic-stats.cpp
@@ -182,6 +182,9 @@ OBSBasicStats::OBSBasicStats(QWidget *parent, bool closeable)
}
obs_frontend_add_event_callback(OBSFrontendEvent, this);
+
+ if (obs_frontend_recording_active())
+ StartRecTimeLeft();
}
void OBSBasicStats::closeEvent(QCloseEvent *event)
@@ -412,15 +415,20 @@ void OBSBasicStats::Update()
void OBSBasicStats::StartRecTimeLeft()
{
+ if (recTimeLeft.isActive())
+ ResetRecTimeLeft();
+
recordTimeLeft->setText(QTStr("Calculating"));
recTimeLeft.start();
}
void OBSBasicStats::ResetRecTimeLeft()
{
- bitrates.clear();
- recTimeLeft.stop();
- recordTimeLeft->setText(QTStr(""));
+ if (recTimeLeft.isActive()) {
+ bitrates.clear();
+ recTimeLeft.stop();
+ recordTimeLeft->setText(QTStr(""));
+ }
}
void OBSBasicStats::RecordingTimeLeft()
diff --git a/UI/window-basic-status-bar.cpp b/UI/window-basic-status-bar.cpp
index eba5354..b53bf7f 100644
--- a/UI/window-basic-status-bar.cpp
+++ b/UI/window-basic-status-bar.cpp
@@ -16,8 +16,8 @@ OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent)
droppedFrames(new QLabel),
streamIcon(new QLabel),
streamTime(new QLabel),
- recordIcon(new QLabel),
recordTime(new QLabel),
+ recordIcon(new QLabel),
cpuUsage(new QLabel),
transparentPixmap(20, 20),
greenPixmap(20, 20),
diff --git a/UI/window-importer.cpp b/UI/window-importer.cpp
index 2bccc52..f887731 100644
--- a/UI/window-importer.cpp
+++ b/UI/window-importer.cpp
@@ -22,7 +22,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -169,7 +168,7 @@ void ImporterEntryPathItemDelegate::handleBrowse(QWidget *container)
QString currentPath = text->text();
bool isSet = false;
- QStringList paths = QFileDialog::getOpenFileNames(
+ QStringList paths = OpenFiles(
container, QTStr("Importer.SelectCollection"), currentPath,
QTStr("Importer.Collection") + QString(" ") + Pattern);
@@ -284,7 +283,7 @@ void ImporterModel::checkInputPath(int row)
std::string program = DetectProgram(entry.path.toStdString());
entry.program = QTStr(program.c_str());
- if (program == "") {
+ if (program.empty()) {
entry.selected = false;
} else {
std::string name =
@@ -535,7 +534,7 @@ void OBSImporter::browseImport()
{
QString Pattern = "(*.json *.bpres *.xml *.xconfig)";
- QStringList paths = QFileDialog::getOpenFileNames(
+ QStringList paths = OpenFiles(
this, QTStr("Importer.SelectCollection"), "",
QTStr("Importer.Collection") + QString(" ") + Pattern);
diff --git a/UI/window-log-reply.cpp b/UI/window-log-reply.cpp
index 7be665e..099f857 100644
--- a/UI/window-log-reply.cpp
+++ b/UI/window-log-reply.cpp
@@ -16,14 +16,23 @@
******************************************************************************/
#include
+#include
+#include
+#include
#include "window-log-reply.hpp"
#include "obs-app.hpp"
-OBSLogReply::OBSLogReply(QWidget *parent, const QString &url)
+OBSLogReply::OBSLogReply(QWidget *parent, const QString &url, const bool crash)
: QDialog(parent), ui(new Ui::OBSLogReply)
{
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
ui->setupUi(this);
ui->urlEdit->setText(url);
+ if (crash) {
+ ui->analyzeURL->hide();
+ ui->description->setText(
+ Str("LogReturnDialog.Description.Crash"));
+ }
installEventFilter(CreateShortcutFilter());
}
@@ -33,3 +42,13 @@ void OBSLogReply::on_copyURL_clicked()
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(ui->urlEdit->text());
}
+
+void OBSLogReply::on_analyzeURL_clicked()
+{
+ QUrlQuery param;
+ param.addQueryItem("log_url",
+ QUrl::toPercentEncoding(ui->urlEdit->text()));
+ QUrl url("https://obsproject.com/tools/analyzer", QUrl::TolerantMode);
+ url.setQuery(param);
+ QDesktopServices::openUrl(url);
+}
diff --git a/UI/window-log-reply.hpp b/UI/window-log-reply.hpp
index 9fcab20..746d1f4 100644
--- a/UI/window-log-reply.hpp
+++ b/UI/window-log-reply.hpp
@@ -27,8 +27,9 @@ class OBSLogReply : public QDialog {
std::unique_ptr ui;
public:
- OBSLogReply(QWidget *parent, const QString &url);
+ OBSLogReply(QWidget *parent, const QString &url, const bool crash);
private slots:
void on_copyURL_clicked();
+ void on_analyzeURL_clicked();
};
diff --git a/UI/window-projector.cpp b/UI/window-projector.cpp
index de39a1f..7b6f6e1 100644
--- a/UI/window-projector.cpp
+++ b/UI/window-projector.cpp
@@ -10,6 +10,8 @@
#include "platform.hpp"
static QList multiviewProjectors;
+static QList allProjectors;
+
static bool updatingMultiview = false, drawLabel, drawSafeArea, mouseSwitching,
transitionOnDoubleClick;
static MultiviewLayout multiviewLayout;
@@ -22,6 +24,12 @@ OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, int monitor,
removedSignal(obs_source_get_signal_handler(source), "remove",
OBSSourceRemoved, this)
{
+ isAlwaysOnTop = config_get_bool(GetGlobalConfig(), "BasicWindow",
+ "ProjectorAlwaysOnTop");
+
+ if (isAlwaysOnTop)
+ setWindowFlags(Qt::WindowStaysOnTopHint);
+
type = type_;
setWindowIcon(QIcon::fromTheme("rpan-studio", QIcon(":/res/images/rpan-studio.png")));
@@ -38,9 +46,6 @@ OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, int monitor,
addAction(action);
connect(action, SIGNAL(triggered()), this, SLOT(EscapeTriggered()));
- SetAlwaysOnTop(this, config_get_bool(GetGlobalConfig(), "BasicWindow",
- "ProjectorAlwaysOnTop"));
-
setAttribute(Qt::WA_DeleteOnClose, true);
//disable application quit when last window closed
@@ -57,6 +62,8 @@ OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, int monitor,
};
connect(this, &OBSQTDisplay::DisplayCreated, addDrawCallback);
+ connect(App(), &QGuiApplication::screenRemoved, this,
+ &OBSProjector::ScreenRemoved);
if (type == ProjectorType::Multiview) {
obs_enter_graphics();
@@ -121,6 +128,8 @@ OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, int monitor,
if (source)
obs_source_inc_showing(source);
+ allProjectors.push_back(this);
+
ready = true;
show();
@@ -160,12 +169,14 @@ OBSProjector::~OBSProjector()
multiviewProjectors.removeAll(this);
App()->DecrementSleepInhibition();
+
+ screen = nullptr;
}
void OBSProjector::SetMonitor(int monitor)
{
savedMonitor = monitor;
- QScreen *screen = QGuiApplication::screens()[monitor];
+ screen = QGuiApplication::screens()[monitor];
setGeometry(screen->geometry());
showFullScreen();
SetHideCursor();
@@ -173,6 +184,9 @@ void OBSProjector::SetMonitor(int monitor)
void OBSProjector::SetHideCursor()
{
+ if (savedMonitor == -1)
+ return;
+
bool hideCursor = config_get_bool(GetGlobalConfig(), "BasicWindow",
"HideProjectorCursor");
@@ -825,10 +839,25 @@ void OBSProjector::mousePressEvent(QMouseEvent *event)
SLOT(OpenFullScreenProjector()));
popup.addMenu(projectorMenu);
- if (GetMonitor() > -1)
+ if (GetMonitor() > -1) {
popup.addAction(QTStr("Windowed"), this,
SLOT(OpenWindowedProjector()));
+ } else if (!this->isMaximized()) {
+ popup.addAction(QTStr("ResizeProjectorWindowToContent"),
+ this, SLOT(ResizeToContent()));
+ }
+
+ QAction *alwaysOnTopButton =
+ new QAction(QTStr("Basic.MainMenu.AlwaysOnTop"), this);
+ alwaysOnTopButton->setCheckable(true);
+ alwaysOnTopButton->setChecked(isAlwaysOnTop);
+
+ connect(alwaysOnTopButton, &QAction::toggled, this,
+ &OBSProjector::AlwaysOnTopToggled);
+
+ popup.addAction(alwaysOnTopButton);
+
popup.addAction(QTStr("Close"), this, SLOT(EscapeTriggered()));
popup.exec(QCursor::pos());
}
@@ -854,6 +883,8 @@ void OBSProjector::EscapeTriggered()
{
OBSBasic *main = reinterpret_cast(App()->GetMainWindow());
main->DeleteProjector(this);
+
+ allProjectors.removeAll(this);
}
void OBSProjector::UpdateMultiview()
@@ -1050,6 +1081,39 @@ void OBSProjector::OpenWindowedProjector()
savedMonitor = -1;
UpdateProjectorTitle(QT_UTF8(obs_source_get_name(source)));
+ screen = nullptr;
+}
+
+void OBSProjector::ResizeToContent()
+{
+ OBSSource source = GetSource();
+ uint32_t targetCX;
+ uint32_t targetCY;
+ int x, y, newX, newY;
+ float scale;
+
+ if (source) {
+ targetCX = std::max(obs_source_get_width(source), 1u);
+ targetCY = std::max(obs_source_get_height(source), 1u);
+ } else {
+ struct obs_video_info ovi;
+ obs_get_video_info(&ovi);
+ targetCX = ovi.base_width;
+ targetCY = ovi.base_height;
+ }
+
+ QSize size = this->size();
+ GetScaleAndCenterPos(targetCX, targetCY, size.width(), size.height(), x,
+ y, scale);
+
+ newX = size.width() - (x * 2);
+ newY = size.height() - (y * 2);
+ resize(newX, newY);
+}
+
+void OBSProjector::AlwaysOnTopToggled(bool isAlwaysOnTop)
+{
+ SetIsAlwaysOnTop(isAlwaysOnTop, true);
}
void OBSProjector::closeEvent(QCloseEvent *event)
@@ -1057,3 +1121,30 @@ void OBSProjector::closeEvent(QCloseEvent *event)
EscapeTriggered();
event->accept();
}
+
+bool OBSProjector::IsAlwaysOnTop() const
+{
+ return isAlwaysOnTop;
+}
+
+bool OBSProjector::IsAlwaysOnTopOverridden() const
+{
+ return isAlwaysOnTopOverridden;
+}
+
+void OBSProjector::SetIsAlwaysOnTop(bool isAlwaysOnTop, bool isOverridden)
+{
+ this->isAlwaysOnTop = isAlwaysOnTop;
+ this->isAlwaysOnTopOverridden = isOverridden;
+
+ SetAlwaysOnTop(this, isAlwaysOnTop);
+}
+
+void OBSProjector::ScreenRemoved(QScreen *screen_)
+{
+ if (GetMonitor() < 0 || !screen)
+ return;
+
+ if (screen == screen_)
+ EscapeTriggered();
+}
diff --git a/UI/window-projector.hpp b/UI/window-projector.hpp
index c1fbb65..599cf67 100644
--- a/UI/window-projector.hpp
+++ b/UI/window-projector.hpp
@@ -36,6 +36,8 @@ class OBSProjector : public OBSQTDisplay {
void mouseDoubleClickEvent(QMouseEvent *event) override;
void closeEvent(QCloseEvent *event) override;
+ bool isAlwaysOnTop;
+ bool isAlwaysOnTopOverridden = false;
int savedMonitor = -1;
ProjectorType type = ProjectorType::Source;
std::vector