1 /******************************************************************************
2 Copyright (C) 2013-2015 by Hugh Bailey <obs.jim@gmail.com>
3 Zachary Lund <admin@computerquip.com>
4 Philippe Groarke <philippe.groarke@gmail.com>
5
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 2 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
18 ******************************************************************************/
19 #include "ui-config.h"
20
21 #include <cstddef>
22 #include <ctime>
23 #include <functional>
24 #include <obs-data.h>
25 #include <obs.h>
26 #include <obs.hpp>
27 #include <QGuiApplication>
28 #include <QMessageBox>
29 #include <QShowEvent>
30 #include <QDesktopServices>
31 #include <QFileDialog>
32 #include <QScreen>
33 #include <QColorDialog>
34 #include <QSizePolicy>
35 #include <QScrollBar>
36 #include <QTextStream>
37
38 #include <util/dstr.h>
39 #include <util/util.hpp>
40 #include <util/platform.h>
41 #include <util/profiler.hpp>
42 #include <util/dstr.hpp>
43
44 #include "obs-app.hpp"
45 #include "platform.hpp"
46 #include "visibility-item-widget.hpp"
47 #include "item-widget-helpers.hpp"
48 #include "window-basic-settings.hpp"
49 #include "window-namedialog.hpp"
50 #include "window-basic-auto-config.hpp"
51 #include "window-basic-source-select.hpp"
52 #include "window-basic-main.hpp"
53 #include "window-basic-stats.hpp"
54 #include "window-basic-main-outputs.hpp"
55 #include "window-log-reply.hpp"
56 #include "window-projector.hpp"
57 #include "window-remux.hpp"
58 #if YOUTUBE_ENABLED
59 #include "auth-youtube.hpp"
60 #include "window-youtube-actions.hpp"
61 #include "youtube-api-wrappers.hpp"
62 #endif
63 #include "qt-wrappers.hpp"
64 #include "context-bar-controls.hpp"
65 #include "obs-proxy-style.hpp"
66 #include "display-helpers.hpp"
67 #include "volume-control.hpp"
68 #include "remote-text.hpp"
69 #include "ui-validation.hpp"
70 #include "media-controls.hpp"
71 #include "undo-stack-obs.hpp"
72 #include <fstream>
73 #include <sstream>
74
75 #ifdef _WIN32
76 #include "win-update/win-update.hpp"
77 #include "windows.h"
78 #endif
79
80 #include "ui_OBSBasic.h"
81 #include "ui_ColorSelect.h"
82
83 #include <QWindow>
84
85 #include <json11.hpp>
86
87 #ifdef ENABLE_WAYLAND
88 #include <obs-nix-platform.h>
89 #endif
90
91 using namespace json11;
92 using namespace std;
93
94 #ifdef BROWSER_AVAILABLE
95 #include <browser-panel.hpp>
96 #endif
97
98 #include "ui-config.h"
99
100 struct QCef;
101 struct QCefCookieManager;
102
103 QCef *cef = nullptr;
104 QCefCookieManager *panel_cookies = nullptr;
105
106 void DestroyPanelCookieManager();
107
108 namespace {
109
110 template<typename OBSRef> struct SignalContainer {
111 OBSRef ref;
112 vector<shared_ptr<OBSSignal>> handlers;
113 };
114 }
115
116 extern volatile long insideEventLoop;
117
118 Q_DECLARE_METATYPE(OBSScene);
119 Q_DECLARE_METATYPE(OBSSceneItem);
120 Q_DECLARE_METATYPE(OBSSource);
121 Q_DECLARE_METATYPE(obs_order_movement);
122 Q_DECLARE_METATYPE(SignalContainer<OBSScene>);
123
operator <<(QDataStream & out,const SignalContainer<OBSScene> & v)124 QDataStream &operator<<(QDataStream &out, const SignalContainer<OBSScene> &v)
125 {
126 out << v.ref;
127 return out;
128 }
129
operator >>(QDataStream & in,SignalContainer<OBSScene> & v)130 QDataStream &operator>>(QDataStream &in, SignalContainer<OBSScene> &v)
131 {
132 in >> v.ref;
133 return in;
134 }
135
GetOBSRef(QListWidgetItem * item)136 template<typename T> static T GetOBSRef(QListWidgetItem *item)
137 {
138 return item->data(static_cast<int>(QtDataRole::OBSRef)).value<T>();
139 }
140
SetOBSRef(QListWidgetItem * item,T && val)141 template<typename T> static void SetOBSRef(QListWidgetItem *item, T &&val)
142 {
143 item->setData(static_cast<int>(QtDataRole::OBSRef),
144 QVariant::fromValue(val));
145 }
146
AddExtraModulePaths()147 static void AddExtraModulePaths()
148 {
149 char *plugins_path = getenv("OBS_PLUGINS_PATH");
150 char *plugins_data_path = getenv("OBS_PLUGINS_DATA_PATH");
151 if (plugins_path && plugins_data_path) {
152 string data_path_with_module_suffix;
153 data_path_with_module_suffix += plugins_data_path;
154 data_path_with_module_suffix += "/%module%";
155 obs_add_module_path(plugins_path,
156 data_path_with_module_suffix.c_str());
157 }
158
159 char base_module_dir[512];
160 #if defined(_WIN32) || defined(__APPLE__)
161 int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir),
162 "obs-studio/plugins/%module%");
163 #else
164 int ret = GetConfigPath(base_module_dir, sizeof(base_module_dir),
165 "obs-studio/plugins/%module%");
166 #endif
167
168 if (ret <= 0)
169 return;
170
171 string path = base_module_dir;
172 #if defined(__APPLE__)
173 obs_add_module_path((path + "/bin").c_str(), (path + "/data").c_str());
174
175 BPtr<char> config_bin =
176 os_get_config_path_ptr("obs-studio/plugins/%module%/bin");
177 BPtr<char> config_data =
178 os_get_config_path_ptr("obs-studio/plugins/%module%/data");
179 obs_add_module_path(config_bin, config_data);
180
181 #elif ARCH_BITS == 64
182 obs_add_module_path((path + "/bin/64bit").c_str(),
183 (path + "/data").c_str());
184 #else
185 obs_add_module_path((path + "/bin/32bit").c_str(),
186 (path + "/data").c_str());
187 #endif
188 }
189
190 extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main);
191
assignDockToggle(QDockWidget * dock,QAction * action)192 void assignDockToggle(QDockWidget *dock, QAction *action)
193 {
194 auto handleWindowToggle = [action](bool vis) {
195 action->blockSignals(true);
196 action->setChecked(vis);
197 action->blockSignals(false);
198 };
199 auto handleMenuToggle = [dock](bool check) {
200 dock->blockSignals(true);
201 dock->setVisible(check);
202 dock->blockSignals(false);
203 };
204
205 dock->connect(dock->toggleViewAction(), &QAction::toggled,
206 handleWindowToggle);
207 dock->connect(action, &QAction::toggled, handleMenuToggle);
208 }
209
210 extern void RegisterTwitchAuth();
211 extern void RegisterRestreamAuth();
212 #if YOUTUBE_ENABLED
213 extern void RegisterYoutubeAuth();
214 #endif
215
OBSBasic(QWidget * parent)216 OBSBasic::OBSBasic(QWidget *parent)
217 : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic)
218 {
219 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
220 qRegisterMetaTypeStreamOperators<SignalContainer<OBSScene>>(
221 "SignalContainer<OBSScene>");
222 #endif
223
224 setAttribute(Qt::WA_NativeWindow);
225
226 #if TWITCH_ENABLED
227 RegisterTwitchAuth();
228 #endif
229 #if RESTREAM_ENABLED
230 RegisterRestreamAuth();
231 #endif
232 #if YOUTUBE_ENABLED
233 RegisterYoutubeAuth();
234 #endif
235
236 setAcceptDrops(true);
237
238 setContextMenuPolicy(Qt::CustomContextMenu);
239 connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this,
240 SLOT(on_customContextMenuRequested(const QPoint &)));
241
242 api = InitializeAPIInterface(this);
243
244 ui->setupUi(this);
245 ui->previewDisabledWidget->setVisible(false);
246 ui->contextContainer->setStyle(new OBSProxyStyle);
247 ui->broadcastButton->setVisible(false);
248
249 startingDockLayout = saveState();
250
251 statsDock = new OBSDock();
252 statsDock->setObjectName(QStringLiteral("statsDock"));
253 statsDock->setFeatures(QDockWidget::DockWidgetClosable |
254 QDockWidget::DockWidgetMovable |
255 QDockWidget::DockWidgetFloatable);
256 statsDock->setWindowTitle(QTStr("Basic.Stats"));
257 addDockWidget(Qt::BottomDockWidgetArea, statsDock);
258 statsDock->setVisible(false);
259 statsDock->setFloating(true);
260 statsDock->resize(700, 200);
261
262 copyActionsDynamicProperties();
263
264 char styleSheetPath[512];
265 int ret = GetProfilePath(styleSheetPath, sizeof(styleSheetPath),
266 "stylesheet.qss");
267 if (ret > 0) {
268 if (QFile::exists(styleSheetPath)) {
269 QString path =
270 QString("file:///") + QT_UTF8(styleSheetPath);
271 App()->setStyleSheet(path);
272 }
273 }
274
275 qRegisterMetaType<int64_t>("int64_t");
276 qRegisterMetaType<uint32_t>("uint32_t");
277 qRegisterMetaType<OBSScene>("OBSScene");
278 qRegisterMetaType<OBSSceneItem>("OBSSceneItem");
279 qRegisterMetaType<OBSSource>("OBSSource");
280 qRegisterMetaType<obs_hotkey_id>("obs_hotkey_id");
281 qRegisterMetaType<SavedProjectorInfo *>("SavedProjectorInfo *");
282
283 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
284 qRegisterMetaTypeStreamOperators<std::vector<std::shared_ptr<OBSSignal>>>(
285 "std::vector<std::shared_ptr<OBSSignal>>");
286 qRegisterMetaTypeStreamOperators<OBSScene>("OBSScene");
287 qRegisterMetaTypeStreamOperators<OBSSceneItem>("OBSSceneItem");
288 #endif
289
290 ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false);
291 ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false);
292
293 bool sceneGrid = config_get_bool(App()->GlobalConfig(), "BasicWindow",
294 "gridMode");
295 ui->scenes->SetGridMode(sceneGrid);
296
297 ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes));
298
299 auto displayResize = [this]() {
300 struct obs_video_info ovi;
301
302 if (obs_get_video_info(&ovi))
303 ResizePreview(ovi.base_width, ovi.base_height);
304 };
305
306 connect(windowHandle(), &QWindow::screenChanged, displayResize);
307 connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize);
308
309 delete shortcutFilter;
310 shortcutFilter = CreateShortcutFilter();
311 installEventFilter(shortcutFilter);
312
313 stringstream name;
314 name << "OBS " << App()->GetVersionString();
315 blog(LOG_INFO, "%s", name.str().c_str());
316 blog(LOG_INFO, "---------------------------------");
317
318 UpdateTitleBar();
319
320 connect(ui->scenes->itemDelegate(),
321 SIGNAL(closeEditor(QWidget *,
322 QAbstractItemDelegate::EndEditHint)),
323 this,
324 SLOT(SceneNameEdited(QWidget *,
325 QAbstractItemDelegate::EndEditHint)));
326
327 cpuUsageInfo = os_cpu_usage_info_start();
328 cpuUsageTimer = new QTimer(this);
329 connect(cpuUsageTimer.data(), SIGNAL(timeout()), ui->statusbar,
330 SLOT(UpdateCPUUsage()));
331 cpuUsageTimer->start(3000);
332
333 diskFullTimer = new QTimer(this);
334 connect(diskFullTimer, SIGNAL(timeout()), this,
335 SLOT(CheckDiskSpaceRemaining()));
336
337 renameScene = new QAction(ui->scenesDock);
338 renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut);
339 connect(renameScene, SIGNAL(triggered()), this, SLOT(EditSceneName()));
340 ui->scenesDock->addAction(renameScene);
341
342 renameSource = new QAction(ui->sourcesDock);
343 renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut);
344 connect(renameSource, SIGNAL(triggered()), this,
345 SLOT(EditSceneItemName()));
346 ui->sourcesDock->addAction(renameSource);
347
348 #ifdef __APPLE__
349 renameScene->setShortcut({Qt::Key_Return});
350 renameSource->setShortcut({Qt::Key_Return});
351
352 ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace});
353 ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace});
354
355 ui->action_Settings->setMenuRole(QAction::PreferencesRole);
356 ui->actionE_xit->setMenuRole(QAction::QuitRole);
357 #else
358 renameScene->setShortcut({Qt::Key_F2});
359 renameSource->setShortcut({Qt::Key_F2});
360 #endif
361
362 #ifdef __linux__
363 ui->actionE_xit->setShortcut(Qt::CTRL + Qt::Key_Q);
364 #endif
365
366 auto addNudge = [this](const QKeySequence &seq, const char *s) {
367 QAction *nudge = new QAction(ui->preview);
368 nudge->setShortcut(seq);
369 nudge->setShortcutContext(Qt::WidgetShortcut);
370 ui->preview->addAction(nudge);
371 connect(nudge, SIGNAL(triggered()), this, s);
372 };
373
374 addNudge(Qt::Key_Up, SLOT(NudgeUp()));
375 addNudge(Qt::Key_Down, SLOT(NudgeDown()));
376 addNudge(Qt::Key_Left, SLOT(NudgeLeft()));
377 addNudge(Qt::Key_Right, SLOT(NudgeRight()));
378
379 assignDockToggle(ui->scenesDock, ui->toggleScenes);
380 assignDockToggle(ui->sourcesDock, ui->toggleSources);
381 assignDockToggle(ui->mixerDock, ui->toggleMixer);
382 assignDockToggle(ui->transitionsDock, ui->toggleTransitions);
383 assignDockToggle(ui->controlsDock, ui->toggleControls);
384 assignDockToggle(statsDock, ui->toggleStats);
385
386 // Register shortcuts for Undo/Redo
387 ui->actionMainUndo->setShortcut(Qt::CTRL + Qt::Key_Z);
388 QList<QKeySequence> shrt;
389 shrt << QKeySequence(Qt::CTRL | Qt::SHIFT + Qt::Key_Z)
390 << QKeySequence(Qt::CTRL + Qt::Key_Y);
391 ui->actionMainRedo->setShortcuts(shrt);
392
393 ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut);
394 ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut);
395
396 //hide all docking panes
397 ui->toggleScenes->setChecked(false);
398 ui->toggleSources->setChecked(false);
399 ui->toggleMixer->setChecked(false);
400 ui->toggleTransitions->setChecked(false);
401 ui->toggleControls->setChecked(false);
402 ui->toggleStats->setChecked(false);
403
404 QPoint curPos;
405
406 //restore parent window geometry
407 const char *geometry = config_get_string(App()->GlobalConfig(),
408 "BasicWindow", "geometry");
409 if (geometry != NULL) {
410 QByteArray byteArray =
411 QByteArray::fromBase64(QByteArray(geometry));
412 restoreGeometry(byteArray);
413
414 QRect windowGeometry = normalGeometry();
415 if (!WindowPositionValid(windowGeometry)) {
416 QRect rect =
417 QGuiApplication::primaryScreen()->geometry();
418 setGeometry(QStyle::alignedRect(Qt::LeftToRight,
419 Qt::AlignCenter, size(),
420 rect));
421 }
422
423 curPos = pos();
424 } else {
425 QRect desktopRect =
426 QGuiApplication::primaryScreen()->geometry();
427 QSize adjSize = desktopRect.size() / 2 - size() / 2;
428 curPos = QPoint(adjSize.width(), adjSize.height());
429 }
430
431 QPoint curSize(width(), height());
432
433 QPoint statsDockSize(statsDock->width(), statsDock->height());
434 QPoint statsDockPos = curSize / 2 - statsDockSize / 2;
435 QPoint newPos = curPos + statsDockPos;
436 statsDock->move(newPos);
437
438 ui->previewLabel->setProperty("themeID", "previewProgramLabels");
439 ui->previewLabel->style()->polish(ui->previewLabel);
440
441 bool labels = config_get_bool(GetGlobalConfig(), "BasicWindow",
442 "StudioModeLabels");
443
444 if (!previewProgramMode)
445 ui->previewLabel->setHidden(true);
446 else
447 ui->previewLabel->setHidden(!labels);
448
449 ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu);
450 connect(ui->previewDisabledWidget,
451 SIGNAL(customContextMenuRequested(const QPoint &)), this,
452 SLOT(PreviewDisabledMenu(const QPoint &)));
453 connect(ui->enablePreviewButton, SIGNAL(clicked()), this,
454 SLOT(TogglePreview()));
455
456 connect(ui->scenes, SIGNAL(scenesReordered()), this,
457 SLOT(ScenesReordered()));
458
459 connect(ui->broadcastButton, &QPushButton::clicked, this,
460 &OBSBasic::BroadcastButtonClicked);
461
462 UpdatePreviewSafeAreas();
463 }
464
SaveAudioDevice(const char * name,int channel,obs_data_t * parent,vector<OBSSource> & audioSources)465 static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent,
466 vector<OBSSource> &audioSources)
467 {
468 obs_source_t *source = obs_get_output_source(channel);
469 if (!source)
470 return;
471
472 audioSources.push_back(source);
473
474 obs_data_t *data = obs_save_source(source);
475
476 obs_data_set_obj(parent, name, data);
477
478 obs_data_release(data);
479 obs_source_release(source);
480 }
481
GenerateSaveData(obs_data_array_t * sceneOrder,obs_data_array_t * quickTransitionData,int transitionDuration,obs_data_array_t * transitions,OBSScene & scene,OBSSource & curProgramScene,obs_data_array_t * savedProjectorList)482 static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder,
483 obs_data_array_t *quickTransitionData,
484 int transitionDuration,
485 obs_data_array_t *transitions,
486 OBSScene &scene, OBSSource &curProgramScene,
487 obs_data_array_t *savedProjectorList)
488 {
489 obs_data_t *saveData = obs_data_create();
490
491 vector<OBSSource> audioSources;
492 audioSources.reserve(6);
493
494 SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources);
495 SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources);
496 SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources);
497 SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources);
498 SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources);
499 SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources);
500
501 /* -------------------------------- */
502 /* save non-group sources */
503
504 auto FilterAudioSources = [&](obs_source_t *source) {
505 if (obs_source_is_group(source))
506 return false;
507
508 return find(begin(audioSources), end(audioSources), source) ==
509 end(audioSources);
510 };
511 using FilterAudioSources_t = decltype(FilterAudioSources);
512
513 obs_data_array_t *sourcesArray = obs_save_sources_filtered(
514 [](void *data, obs_source_t *source) {
515 return (*static_cast<FilterAudioSources_t *>(data))(
516 source);
517 },
518 static_cast<void *>(&FilterAudioSources));
519
520 /* -------------------------------- */
521 /* save group sources separately */
522
523 /* saving separately ensures they won't be loaded in older versions */
524 obs_data_array_t *groupsArray = obs_save_sources_filtered(
525 [](void *, obs_source_t *source) {
526 return obs_source_is_group(source);
527 },
528 nullptr);
529
530 /* -------------------------------- */
531
532 obs_source_t *transition = obs_get_output_source(0);
533 obs_source_t *currentScene = obs_scene_get_source(scene);
534 const char *sceneName = obs_source_get_name(currentScene);
535 const char *programName = obs_source_get_name(curProgramScene);
536
537 const char *sceneCollection = config_get_string(
538 App()->GlobalConfig(), "Basic", "SceneCollection");
539
540 obs_data_set_string(saveData, "current_scene", sceneName);
541 obs_data_set_string(saveData, "current_program_scene", programName);
542 obs_data_set_array(saveData, "scene_order", sceneOrder);
543 obs_data_set_string(saveData, "name", sceneCollection);
544 obs_data_set_array(saveData, "sources", sourcesArray);
545 obs_data_set_array(saveData, "groups", groupsArray);
546 obs_data_set_array(saveData, "quick_transitions", quickTransitionData);
547 obs_data_set_array(saveData, "transitions", transitions);
548 obs_data_set_array(saveData, "saved_projectors", savedProjectorList);
549 obs_data_array_release(sourcesArray);
550 obs_data_array_release(groupsArray);
551
552 obs_data_set_string(saveData, "current_transition",
553 obs_source_get_name(transition));
554 obs_data_set_int(saveData, "transition_duration", transitionDuration);
555 obs_source_release(transition);
556
557 return saveData;
558 }
559
copyActionsDynamicProperties()560 void OBSBasic::copyActionsDynamicProperties()
561 {
562 // Themes need the QAction dynamic properties
563 for (QAction *x : ui->scenesToolbar->actions()) {
564 QWidget *temp = ui->scenesToolbar->widgetForAction(x);
565
566 for (QByteArray &y : x->dynamicPropertyNames()) {
567 temp->setProperty(y, x->property(y));
568 }
569 }
570
571 for (QAction *x : ui->sourcesToolbar->actions()) {
572 QWidget *temp = ui->sourcesToolbar->widgetForAction(x);
573
574 for (QByteArray &y : x->dynamicPropertyNames()) {
575 temp->setProperty(y, x->property(y));
576 }
577 }
578 }
579
UpdateVolumeControlsDecayRate()580 void OBSBasic::UpdateVolumeControlsDecayRate()
581 {
582 double meterDecayRate =
583 config_get_double(basicConfig, "Audio", "MeterDecayRate");
584
585 for (size_t i = 0; i < volumes.size(); i++) {
586 volumes[i]->SetMeterDecayRate(meterDecayRate);
587 }
588 }
589
UpdateVolumeControlsPeakMeterType()590 void OBSBasic::UpdateVolumeControlsPeakMeterType()
591 {
592 uint32_t peakMeterTypeIdx =
593 config_get_uint(basicConfig, "Audio", "PeakMeterType");
594
595 enum obs_peak_meter_type peakMeterType;
596 switch (peakMeterTypeIdx) {
597 case 0:
598 peakMeterType = SAMPLE_PEAK_METER;
599 break;
600 case 1:
601 peakMeterType = TRUE_PEAK_METER;
602 break;
603 default:
604 peakMeterType = SAMPLE_PEAK_METER;
605 break;
606 }
607
608 for (size_t i = 0; i < volumes.size(); i++) {
609 volumes[i]->setPeakMeterType(peakMeterType);
610 }
611 }
612
ClearVolumeControls()613 void OBSBasic::ClearVolumeControls()
614 {
615 for (VolControl *vol : volumes)
616 delete vol;
617
618 volumes.clear();
619 }
620
SaveSceneListOrder()621 obs_data_array_t *OBSBasic::SaveSceneListOrder()
622 {
623 obs_data_array_t *sceneOrder = obs_data_array_create();
624
625 for (int i = 0; i < ui->scenes->count(); i++) {
626 obs_data_t *data = obs_data_create();
627 obs_data_set_string(data, "name",
628 QT_TO_UTF8(ui->scenes->item(i)->text()));
629 obs_data_array_push_back(sceneOrder, data);
630 obs_data_release(data);
631 }
632
633 return sceneOrder;
634 }
635
SaveProjectors()636 obs_data_array_t *OBSBasic::SaveProjectors()
637 {
638 obs_data_array_t *savedProjectors = obs_data_array_create();
639
640 auto saveProjector = [savedProjectors](OBSProjector *projector) {
641 if (!projector)
642 return;
643
644 obs_data_t *data = obs_data_create();
645 ProjectorType type = projector->GetProjectorType();
646
647 switch (type) {
648 case ProjectorType::Scene:
649 case ProjectorType::Source: {
650 obs_source_t *source = projector->GetSource();
651 const char *name = obs_source_get_name(source);
652 obs_data_set_string(data, "name", name);
653 break;
654 }
655 default:
656 break;
657 }
658
659 obs_data_set_int(data, "monitor", projector->GetMonitor());
660 obs_data_set_int(data, "type", static_cast<int>(type));
661 obs_data_set_string(
662 data, "geometry",
663 projector->saveGeometry().toBase64().constData());
664
665 if (projector->IsAlwaysOnTopOverridden())
666 obs_data_set_bool(data, "alwaysOnTop",
667 projector->IsAlwaysOnTop());
668
669 obs_data_set_bool(data, "alwaysOnTopOverridden",
670 projector->IsAlwaysOnTopOverridden());
671
672 obs_data_array_push_back(savedProjectors, data);
673 obs_data_release(data);
674 };
675
676 for (size_t i = 0; i < projectors.size(); i++)
677 saveProjector(static_cast<OBSProjector *>(projectors[i]));
678
679 return savedProjectors;
680 }
681
Save(const char * file)682 void OBSBasic::Save(const char *file)
683 {
684 OBSScene scene = GetCurrentScene();
685 OBSSource curProgramScene = OBSGetStrongRef(programScene);
686 if (!curProgramScene)
687 curProgramScene = obs_scene_get_source(scene);
688
689 obs_data_array_t *sceneOrder = SaveSceneListOrder();
690 obs_data_array_t *transitions = SaveTransitions();
691 obs_data_array_t *quickTrData = SaveQuickTransitions();
692 obs_data_array_t *savedProjectorList = SaveProjectors();
693 obs_data_t *saveData = GenerateSaveData(
694 sceneOrder, quickTrData, ui->transitionDuration->value(),
695 transitions, scene, curProgramScene, savedProjectorList);
696
697 obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked());
698 obs_data_set_bool(saveData, "scaling_enabled",
699 ui->preview->IsFixedScaling());
700 obs_data_set_int(saveData, "scaling_level",
701 ui->preview->GetScalingLevel());
702 obs_data_set_double(saveData, "scaling_off_x",
703 ui->preview->GetScrollX());
704 obs_data_set_double(saveData, "scaling_off_y",
705 ui->preview->GetScrollY());
706
707 if (api) {
708 obs_data_t *moduleObj = obs_data_create();
709 api->on_save(moduleObj);
710 obs_data_set_obj(saveData, "modules", moduleObj);
711 obs_data_release(moduleObj);
712 }
713
714 if (!obs_data_save_json_safe(saveData, file, "tmp", "bak"))
715 blog(LOG_ERROR, "Could not save scene data to %s", file);
716
717 obs_data_release(saveData);
718 obs_data_array_release(sceneOrder);
719 obs_data_array_release(quickTrData);
720 obs_data_array_release(transitions);
721 obs_data_array_release(savedProjectorList);
722 }
723
DeferSaveBegin()724 void OBSBasic::DeferSaveBegin()
725 {
726 os_atomic_inc_long(&disableSaving);
727 }
728
DeferSaveEnd()729 void OBSBasic::DeferSaveEnd()
730 {
731 long result = os_atomic_dec_long(&disableSaving);
732 if (result == 0) {
733 SaveProject();
734 }
735 }
736
737 static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val);
738
LoadAudioDevice(const char * name,int channel,obs_data_t * parent)739 static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent)
740 {
741 obs_data_t *data = obs_data_get_obj(parent, name);
742 if (!data)
743 return;
744
745 obs_source_t *source = obs_load_source(data);
746 if (source) {
747 obs_set_output_source(channel, source);
748
749 const char *name = obs_source_get_name(source);
750 blog(LOG_INFO, "[Loaded global audio device]: '%s'", name);
751 obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1);
752 obs_monitoring_type monitoring_type =
753 obs_source_get_monitoring_type(source);
754 if (monitoring_type != OBS_MONITORING_TYPE_NONE) {
755 const char *type = (monitoring_type ==
756 OBS_MONITORING_TYPE_MONITOR_ONLY)
757 ? "monitor only"
758 : "monitor and output";
759
760 blog(LOG_INFO, " - monitoring: %s", type);
761 }
762 obs_source_release(source);
763 }
764
765 obs_data_release(data);
766 }
767
HasAudioDevices(const char * source_id)768 static inline bool HasAudioDevices(const char *source_id)
769 {
770 const char *output_id = source_id;
771 obs_properties_t *props = obs_get_source_properties(output_id);
772 size_t count = 0;
773
774 if (!props)
775 return false;
776
777 obs_property_t *devices = obs_properties_get(props, "device_id");
778 if (devices)
779 count = obs_property_list_item_count(devices);
780
781 obs_properties_destroy(props);
782
783 return count != 0;
784 }
785
CreateFirstRunSources()786 void OBSBasic::CreateFirstRunSources()
787 {
788 bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource());
789 bool hasInputAudio = HasAudioDevices(App()->InputAudioSource());
790
791 if (hasDesktopAudio)
792 ResetAudioDevice(App()->OutputAudioSource(), "default",
793 Str("Basic.DesktopDevice1"), 1);
794 if (hasInputAudio)
795 ResetAudioDevice(App()->InputAudioSource(), "default",
796 Str("Basic.AuxDevice1"), 3);
797 }
798
CreateDefaultScene(bool firstStart)799 void OBSBasic::CreateDefaultScene(bool firstStart)
800 {
801 disableSaving++;
802
803 ClearSceneData();
804 InitDefaultTransitions();
805 CreateDefaultQuickTransitions();
806 ui->transitionDuration->setValue(300);
807 SetTransition(fadeTransition);
808
809 obs_scene_t *scene = obs_scene_create(Str("Basic.Scene"));
810
811 if (firstStart)
812 CreateFirstRunSources();
813
814 SetCurrentScene(scene, true);
815 obs_scene_release(scene);
816
817 disableSaving--;
818 }
819
ReorderItemByName(QListWidget * lw,const char * name,int newIndex)820 static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex)
821 {
822 for (int i = 0; i < lw->count(); i++) {
823 QListWidgetItem *item = lw->item(i);
824
825 if (strcmp(name, QT_TO_UTF8(item->text())) == 0) {
826 if (newIndex != i) {
827 item = lw->takeItem(i);
828 lw->insertItem(newIndex, item);
829 }
830 break;
831 }
832 }
833 }
834
LoadSceneListOrder(obs_data_array_t * array)835 void OBSBasic::LoadSceneListOrder(obs_data_array_t *array)
836 {
837 size_t num = obs_data_array_count(array);
838
839 for (size_t i = 0; i < num; i++) {
840 obs_data_t *data = obs_data_array_item(array, i);
841 const char *name = obs_data_get_string(data, "name");
842
843 ReorderItemByName(ui->scenes, name, (int)i);
844
845 obs_data_release(data);
846 }
847 }
848
LoadSavedProjectors(obs_data_array_t * array)849 void OBSBasic::LoadSavedProjectors(obs_data_array_t *array)
850 {
851 for (SavedProjectorInfo *info : savedProjectorsArray) {
852 delete info;
853 }
854 savedProjectorsArray.clear();
855
856 size_t num = obs_data_array_count(array);
857
858 for (size_t i = 0; i < num; i++) {
859 obs_data_t *data = obs_data_array_item(array, i);
860
861 SavedProjectorInfo *info = new SavedProjectorInfo();
862 info->monitor = obs_data_get_int(data, "monitor");
863 info->type = static_cast<ProjectorType>(
864 obs_data_get_int(data, "type"));
865 info->geometry =
866 std::string(obs_data_get_string(data, "geometry"));
867 info->name = std::string(obs_data_get_string(data, "name"));
868 info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop");
869 info->alwaysOnTopOverridden =
870 obs_data_get_bool(data, "alwaysOnTopOverridden");
871
872 savedProjectorsArray.emplace_back(info);
873
874 obs_data_release(data);
875 }
876 }
877
LogFilter(obs_source_t *,obs_source_t * filter,void * v_val)878 static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val)
879 {
880 const char *name = obs_source_get_name(filter);
881 const char *id = obs_source_get_id(filter);
882 int val = (int)(intptr_t)v_val;
883 string indent;
884
885 for (int i = 0; i < val; i++)
886 indent += " ";
887
888 blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id);
889 }
890
LogSceneItem(obs_scene_t *,obs_sceneitem_t * item,void * v_val)891 static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val)
892 {
893 obs_source_t *source = obs_sceneitem_get_source(item);
894 const char *name = obs_source_get_name(source);
895 const char *id = obs_source_get_id(source);
896 int indent_count = (int)(intptr_t)v_val;
897 string indent;
898
899 for (int i = 0; i < indent_count; i++)
900 indent += " ";
901
902 blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id);
903
904 obs_monitoring_type monitoring_type =
905 obs_source_get_monitoring_type(source);
906
907 if (monitoring_type != OBS_MONITORING_TYPE_NONE) {
908 const char *type =
909 (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY)
910 ? "monitor only"
911 : "monitor and output";
912
913 blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type);
914 }
915 int child_indent = 1 + indent_count;
916 obs_source_enum_filters(source, LogFilter,
917 (void *)(intptr_t)child_indent);
918
919 obs_source_t *show_tn = obs_sceneitem_get_show_transition(item);
920 obs_source_t *hide_tn = obs_sceneitem_get_hide_transition(item);
921 if (show_tn)
922 blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(),
923 obs_source_get_name(show_tn), obs_source_get_id(show_tn));
924 if (hide_tn)
925 blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(),
926 obs_source_get_name(hide_tn), obs_source_get_id(hide_tn));
927
928 if (obs_sceneitem_is_group(item))
929 obs_sceneitem_group_enum_items(item, LogSceneItem,
930 (void *)(intptr_t)child_indent);
931 return true;
932 }
933
LogScenes()934 void OBSBasic::LogScenes()
935 {
936 blog(LOG_INFO, "------------------------------------------------");
937 blog(LOG_INFO, "Loaded scenes:");
938
939 for (int i = 0; i < ui->scenes->count(); i++) {
940 QListWidgetItem *item = ui->scenes->item(i);
941 OBSScene scene = GetOBSRef<OBSScene>(item);
942
943 obs_source_t *source = obs_scene_get_source(scene);
944 const char *name = obs_source_get_name(source);
945
946 blog(LOG_INFO, "- scene '%s':", name);
947 obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1);
948 obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1);
949 }
950
951 blog(LOG_INFO, "------------------------------------------------");
952 }
953
Load(const char * file)954 void OBSBasic::Load(const char *file)
955 {
956 disableSaving++;
957
958 obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak");
959 if (!data) {
960 disableSaving--;
961 blog(LOG_INFO, "No scene file found, creating default scene");
962 CreateDefaultScene(true);
963 SaveProject();
964 return;
965 }
966
967 LoadData(data, file);
968 }
969
LoadData(obs_data_t * data,const char * file)970 void OBSBasic::LoadData(obs_data_t *data, const char *file)
971 {
972 ClearSceneData();
973 InitDefaultTransitions();
974 ClearContextBar();
975
976 if (devicePropertiesThread && devicePropertiesThread->isRunning()) {
977 devicePropertiesThread->wait();
978 devicePropertiesThread.reset();
979 }
980
981 QApplication::sendPostedEvents(this);
982
983 obs_data_t *modulesObj = obs_data_get_obj(data, "modules");
984 if (api)
985 api->on_preload(modulesObj);
986
987 obs_data_array_t *sceneOrder = obs_data_get_array(data, "scene_order");
988 obs_data_array_t *sources = obs_data_get_array(data, "sources");
989 obs_data_array_t *groups = obs_data_get_array(data, "groups");
990 obs_data_array_t *transitions = obs_data_get_array(data, "transitions");
991 const char *sceneName = obs_data_get_string(data, "current_scene");
992 const char *programSceneName =
993 obs_data_get_string(data, "current_program_scene");
994 const char *transitionName =
995 obs_data_get_string(data, "current_transition");
996
997 if (!opt_starting_scene.empty()) {
998 programSceneName = opt_starting_scene.c_str();
999 if (!IsPreviewProgramMode())
1000 sceneName = opt_starting_scene.c_str();
1001 }
1002
1003 int newDuration = obs_data_get_int(data, "transition_duration");
1004 if (!newDuration)
1005 newDuration = 300;
1006
1007 if (!transitionName)
1008 transitionName = obs_source_get_name(fadeTransition);
1009
1010 const char *curSceneCollection = config_get_string(
1011 App()->GlobalConfig(), "Basic", "SceneCollection");
1012
1013 obs_data_set_default_string(data, "name", curSceneCollection);
1014
1015 const char *name = obs_data_get_string(data, "name");
1016 obs_source_t *curScene;
1017 obs_source_t *curProgramScene;
1018 obs_source_t *curTransition;
1019
1020 if (!name || !*name)
1021 name = curSceneCollection;
1022
1023 LoadAudioDevice(DESKTOP_AUDIO_1, 1, data);
1024 LoadAudioDevice(DESKTOP_AUDIO_2, 2, data);
1025 LoadAudioDevice(AUX_AUDIO_1, 3, data);
1026 LoadAudioDevice(AUX_AUDIO_2, 4, data);
1027 LoadAudioDevice(AUX_AUDIO_3, 5, data);
1028 LoadAudioDevice(AUX_AUDIO_4, 6, data);
1029
1030 if (!sources) {
1031 sources = groups;
1032 groups = nullptr;
1033 } else {
1034 obs_data_array_push_back_array(sources, groups);
1035 }
1036
1037 obs_missing_files_t *files = obs_missing_files_create();
1038
1039 auto cb = [](void *private_data, obs_source_t *source) {
1040 obs_missing_files_t *f = (obs_missing_files_t *)private_data;
1041 obs_missing_files_t *sf = obs_source_get_missing_files(source);
1042
1043 obs_missing_files_append(f, sf);
1044 obs_missing_files_destroy(sf);
1045
1046 UNUSED_PARAMETER(source);
1047 };
1048
1049 obs_load_sources(sources, cb, files);
1050
1051 if (transitions)
1052 LoadTransitions(transitions, cb, files);
1053 if (sceneOrder)
1054 LoadSceneListOrder(sceneOrder);
1055
1056 obs_data_array_release(transitions);
1057
1058 curTransition = FindTransition(transitionName);
1059 if (!curTransition)
1060 curTransition = fadeTransition;
1061
1062 ui->transitionDuration->setValue(newDuration);
1063 SetTransition(curTransition);
1064
1065 retryScene:
1066 curScene = obs_get_source_by_name(sceneName);
1067 curProgramScene = obs_get_source_by_name(programSceneName);
1068
1069 /* if the starting scene command line parameter is bad at all,
1070 * fall back to original settings */
1071 if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) {
1072 sceneName = obs_data_get_string(data, "current_scene");
1073 programSceneName =
1074 obs_data_get_string(data, "current_program_scene");
1075 obs_source_release(curScene);
1076 obs_source_release(curProgramScene);
1077 opt_starting_scene.clear();
1078 goto retryScene;
1079 }
1080
1081 if (!curProgramScene) {
1082 curProgramScene = curScene;
1083 obs_source_addref(curScene);
1084 }
1085
1086 SetCurrentScene(curScene, true);
1087 if (IsPreviewProgramMode())
1088 TransitionToScene(curProgramScene, true);
1089 obs_source_release(curScene);
1090 obs_source_release(curProgramScene);
1091
1092 obs_data_array_release(sources);
1093 obs_data_array_release(groups);
1094 obs_data_array_release(sceneOrder);
1095
1096 /* ------------------- */
1097
1098 bool projectorSave = config_get_bool(GetGlobalConfig(), "BasicWindow",
1099 "SaveProjectors");
1100
1101 if (projectorSave) {
1102 obs_data_array_t *savedProjectors =
1103 obs_data_get_array(data, "saved_projectors");
1104
1105 if (savedProjectors) {
1106 LoadSavedProjectors(savedProjectors);
1107 OpenSavedProjectors();
1108 activateWindow();
1109 }
1110
1111 obs_data_array_release(savedProjectors);
1112 }
1113
1114 /* ------------------- */
1115
1116 std::string file_base = strrchr(file, '/') + 1;
1117 file_base.erase(file_base.size() - 5, 5);
1118
1119 config_set_string(App()->GlobalConfig(), "Basic", "SceneCollection",
1120 name);
1121 config_set_string(App()->GlobalConfig(), "Basic", "SceneCollectionFile",
1122 file_base.c_str());
1123
1124 obs_data_array_t *quickTransitionData =
1125 obs_data_get_array(data, "quick_transitions");
1126 LoadQuickTransitions(quickTransitionData);
1127 obs_data_array_release(quickTransitionData);
1128
1129 RefreshQuickTransitions();
1130
1131 bool previewLocked = obs_data_get_bool(data, "preview_locked");
1132 ui->preview->SetLocked(previewLocked);
1133 ui->actionLockPreview->setChecked(previewLocked);
1134
1135 /* ---------------------- */
1136
1137 bool fixedScaling = obs_data_get_bool(data, "scaling_enabled");
1138 int scalingLevel = (int)obs_data_get_int(data, "scaling_level");
1139 float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x");
1140 float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y");
1141
1142 if (fixedScaling) {
1143 ui->preview->SetScalingLevel(scalingLevel);
1144 ui->preview->SetScrollingOffset(scrollOffX, scrollOffY);
1145 }
1146 ui->preview->SetFixedScaling(fixedScaling);
1147 emit ui->preview->DisplayResized();
1148
1149 /* ---------------------- */
1150
1151 if (api)
1152 api->on_load(modulesObj);
1153
1154 obs_data_release(modulesObj);
1155 obs_data_release(data);
1156
1157 if (!opt_starting_scene.empty())
1158 opt_starting_scene.clear();
1159
1160 if (opt_start_streaming) {
1161 blog(LOG_INFO, "Starting stream due to command line parameter");
1162 QMetaObject::invokeMethod(this, "StartStreaming",
1163 Qt::QueuedConnection);
1164 opt_start_streaming = false;
1165 }
1166
1167 if (opt_start_recording) {
1168 blog(LOG_INFO,
1169 "Starting recording due to command line parameter");
1170 QMetaObject::invokeMethod(this, "StartRecording",
1171 Qt::QueuedConnection);
1172 opt_start_recording = false;
1173 }
1174
1175 if (opt_start_replaybuffer) {
1176 QMetaObject::invokeMethod(this, "StartReplayBuffer",
1177 Qt::QueuedConnection);
1178 opt_start_replaybuffer = false;
1179 }
1180
1181 if (opt_start_virtualcam) {
1182 QMetaObject::invokeMethod(this, "StartVirtualCam",
1183 Qt::QueuedConnection);
1184 opt_start_virtualcam = false;
1185 }
1186
1187 LogScenes();
1188
1189 if (obs_missing_files_count(files) > 0 &&
1190 !App()->IsMissingFilesCheckDisabled()) {
1191 /* the window hasn't fully initialized by this point on macOS,
1192 * so put this at the end of the current task queue. Fixes a
1193 * bug where the window be behind OBS on startup */
1194 QTimer::singleShot(0, [this, files] {
1195 missDialog = new OBSMissingFiles(files, this);
1196 missDialog->setAttribute(Qt::WA_DeleteOnClose, true);
1197 missDialog->show();
1198 missDialog->raise();
1199 });
1200 } else {
1201 obs_missing_files_destroy(files);
1202 }
1203
1204 disableSaving--;
1205
1206 if (api) {
1207 api->on_event(OBS_FRONTEND_EVENT_SCENE_CHANGED);
1208 api->on_event(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED);
1209 }
1210 }
1211
1212 #define SERVICE_PATH "service.json"
1213
SaveService()1214 void OBSBasic::SaveService()
1215 {
1216 if (!service)
1217 return;
1218
1219 char serviceJsonPath[512];
1220 int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath),
1221 SERVICE_PATH);
1222 if (ret <= 0)
1223 return;
1224
1225 obs_data_t *data = obs_data_create();
1226 obs_data_t *settings = obs_service_get_settings(service);
1227
1228 obs_data_set_string(data, "type", obs_service_get_type(service));
1229 obs_data_set_obj(data, "settings", settings);
1230
1231 if (!obs_data_save_json_safe(data, serviceJsonPath, "tmp", "bak"))
1232 blog(LOG_WARNING, "Failed to save service");
1233
1234 obs_data_release(settings);
1235 obs_data_release(data);
1236 }
1237
LoadService()1238 bool OBSBasic::LoadService()
1239 {
1240 const char *type;
1241
1242 char serviceJsonPath[512];
1243 int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath),
1244 SERVICE_PATH);
1245 if (ret <= 0)
1246 return false;
1247
1248 obs_data_t *data =
1249 obs_data_create_from_json_file_safe(serviceJsonPath, "bak");
1250
1251 if (!data)
1252 return false;
1253
1254 obs_data_set_default_string(data, "type", "rtmp_common");
1255 type = obs_data_get_string(data, "type");
1256
1257 obs_data_t *settings = obs_data_get_obj(data, "settings");
1258 obs_data_t *hotkey_data = obs_data_get_obj(data, "hotkeys");
1259
1260 service = obs_service_create(type, "default_service", settings,
1261 hotkey_data);
1262 obs_service_release(service);
1263
1264 obs_data_release(hotkey_data);
1265 obs_data_release(settings);
1266 obs_data_release(data);
1267
1268 return !!service;
1269 }
1270
InitService()1271 bool OBSBasic::InitService()
1272 {
1273 ProfileScope("OBSBasic::InitService");
1274
1275 if (LoadService())
1276 return true;
1277
1278 service = obs_service_create("rtmp_common", "default_service", nullptr,
1279 nullptr);
1280 if (!service)
1281 return false;
1282 obs_service_release(service);
1283
1284 return true;
1285 }
1286
1287 static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5,
1288 (1.0 / 0.6), 1.75, 2.0, 2.25,
1289 2.5, 2.75, 3.0, 0.0};
1290
1291 extern void CheckExistingCookieId();
1292
InitBasicConfigDefaults()1293 bool OBSBasic::InitBasicConfigDefaults()
1294 {
1295 QList<QScreen *> screens = QGuiApplication::screens();
1296
1297 if (!screens.size()) {
1298 OBSErrorBox(NULL, "There appears to be no monitors. Er, this "
1299 "technically shouldn't be possible.");
1300 return false;
1301 }
1302
1303 QScreen *primaryScreen = QGuiApplication::primaryScreen();
1304
1305 uint32_t cx = primaryScreen->size().width();
1306 uint32_t cy = primaryScreen->size().height();
1307
1308 cx *= devicePixelRatioF();
1309 cy *= devicePixelRatioF();
1310
1311 bool oldResolutionDefaults = config_get_bool(
1312 App()->GlobalConfig(), "General", "Pre19Defaults");
1313
1314 /* use 1920x1080 for new default base res if main monitor is above
1315 * 1920x1080, but don't apply for people from older builds -- only to
1316 * new users */
1317 if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) {
1318 cx = 1920;
1319 cy = 1080;
1320 }
1321
1322 bool changed = false;
1323
1324 /* ----------------------------------------------------- */
1325 /* move over old FFmpeg track settings */
1326 if (config_has_user_value(basicConfig, "AdvOut", "FFAudioTrack") &&
1327 !config_has_user_value(basicConfig, "AdvOut", "Pre22.1Settings")) {
1328
1329 int track = (int)config_get_int(basicConfig, "AdvOut",
1330 "FFAudioTrack");
1331 config_set_int(basicConfig, "AdvOut", "FFAudioMixes",
1332 1LL << (track - 1));
1333 config_set_bool(basicConfig, "AdvOut", "Pre22.1Settings", true);
1334 changed = true;
1335 }
1336
1337 /* ----------------------------------------------------- */
1338 /* move over mixer values in advanced if older config */
1339 if (config_has_user_value(basicConfig, "AdvOut", "RecTrackIndex") &&
1340 !config_has_user_value(basicConfig, "AdvOut", "RecTracks")) {
1341
1342 uint64_t track =
1343 config_get_uint(basicConfig, "AdvOut", "RecTrackIndex");
1344 track = 1ULL << (track - 1);
1345 config_set_uint(basicConfig, "AdvOut", "RecTracks", track);
1346 config_remove_value(basicConfig, "AdvOut", "RecTrackIndex");
1347 changed = true;
1348 }
1349
1350 /* ----------------------------------------------------- */
1351 /* set twitch chat extensions to "both" if prev version */
1352 /* is under 24.1 */
1353 if (config_get_bool(GetGlobalConfig(), "General", "Pre24.1Defaults") &&
1354 !config_has_user_value(basicConfig, "Twitch", "AddonChoice")) {
1355 config_set_int(basicConfig, "Twitch", "AddonChoice", 3);
1356 changed = true;
1357 }
1358
1359 /* ----------------------------------------------------- */
1360 /* move bitrate enforcement setting to new value */
1361 if (config_has_user_value(basicConfig, "SimpleOutput",
1362 "EnforceBitrate") &&
1363 !config_has_user_value(basicConfig, "Stream1",
1364 "IgnoreRecommended") &&
1365 !config_has_user_value(basicConfig, "Stream1", "MovedOldEnforce")) {
1366 bool enforce = config_get_bool(basicConfig, "SimpleOutput",
1367 "EnforceBitrate");
1368 config_set_bool(basicConfig, "Stream1", "IgnoreRecommended",
1369 !enforce);
1370 config_set_bool(basicConfig, "Stream1", "MovedOldEnforce",
1371 true);
1372 changed = true;
1373 }
1374
1375 /* ----------------------------------------------------- */
1376 /* enforce minimum retry delay of 1 second prior to 27.1 */
1377 if (config_has_user_value(basicConfig, "Output", "RetryDelay")) {
1378 int retryDelay =
1379 config_get_uint(basicConfig, "Output", "RetryDelay");
1380 if (retryDelay < 1) {
1381 config_set_uint(basicConfig, "Output", "RetryDelay", 1);
1382 changed = true;
1383 }
1384 }
1385
1386 /* ----------------------------------------------------- */
1387
1388 if (changed)
1389 config_save_safe(basicConfig, "tmp", nullptr);
1390
1391 /* ----------------------------------------------------- */
1392
1393 config_set_default_string(basicConfig, "Output", "Mode", "Simple");
1394
1395 config_set_default_bool(basicConfig, "Stream1", "IgnoreRecommended",
1396 false);
1397
1398 config_set_default_string(basicConfig, "SimpleOutput", "FilePath",
1399 GetDefaultVideoSavePath().c_str());
1400 config_set_default_string(basicConfig, "SimpleOutput", "RecFormat",
1401 "mkv");
1402 config_set_default_uint(basicConfig, "SimpleOutput", "VBitrate", 2500);
1403 config_set_default_uint(basicConfig, "SimpleOutput", "ABitrate", 160);
1404 config_set_default_bool(basicConfig, "SimpleOutput", "UseAdvanced",
1405 false);
1406 config_set_default_string(basicConfig, "SimpleOutput", "Preset",
1407 "veryfast");
1408 config_set_default_string(basicConfig, "SimpleOutput", "NVENCPreset",
1409 "hq");
1410 config_set_default_string(basicConfig, "SimpleOutput", "RecQuality",
1411 "Stream");
1412 config_set_default_bool(basicConfig, "SimpleOutput", "RecRB", false);
1413 config_set_default_int(basicConfig, "SimpleOutput", "RecRBTime", 20);
1414 config_set_default_int(basicConfig, "SimpleOutput", "RecRBSize", 512);
1415 config_set_default_string(basicConfig, "SimpleOutput", "RecRBPrefix",
1416 "Replay");
1417
1418 config_set_default_bool(basicConfig, "AdvOut", "ApplyServiceSettings",
1419 true);
1420 config_set_default_bool(basicConfig, "AdvOut", "UseRescale", false);
1421 config_set_default_uint(basicConfig, "AdvOut", "TrackIndex", 1);
1422 config_set_default_uint(basicConfig, "AdvOut", "VodTrackIndex", 2);
1423 config_set_default_string(basicConfig, "AdvOut", "Encoder", "obs_x264");
1424
1425 config_set_default_string(basicConfig, "AdvOut", "RecType", "Standard");
1426
1427 config_set_default_string(basicConfig, "AdvOut", "RecFilePath",
1428 GetDefaultVideoSavePath().c_str());
1429 config_set_default_string(basicConfig, "AdvOut", "RecFormat", "mkv");
1430 config_set_default_bool(basicConfig, "AdvOut", "RecUseRescale", false);
1431 config_set_default_uint(basicConfig, "AdvOut", "RecTracks", (1 << 0));
1432 config_set_default_string(basicConfig, "AdvOut", "RecEncoder", "none");
1433 config_set_default_uint(basicConfig, "AdvOut", "FLVTrack", 1);
1434
1435 config_set_default_bool(basicConfig, "AdvOut", "FFOutputToFile", true);
1436 config_set_default_string(basicConfig, "AdvOut", "FFFilePath",
1437 GetDefaultVideoSavePath().c_str());
1438 config_set_default_string(basicConfig, "AdvOut", "FFExtension", "mp4");
1439 config_set_default_uint(basicConfig, "AdvOut", "FFVBitrate", 2500);
1440 config_set_default_uint(basicConfig, "AdvOut", "FFVGOPSize", 250);
1441 config_set_default_bool(basicConfig, "AdvOut", "FFUseRescale", false);
1442 config_set_default_bool(basicConfig, "AdvOut", "FFIgnoreCompat", false);
1443 config_set_default_uint(basicConfig, "AdvOut", "FFABitrate", 160);
1444 config_set_default_uint(basicConfig, "AdvOut", "FFAudioMixes", 1);
1445
1446 config_set_default_uint(basicConfig, "AdvOut", "Track1Bitrate", 160);
1447 config_set_default_uint(basicConfig, "AdvOut", "Track2Bitrate", 160);
1448 config_set_default_uint(basicConfig, "AdvOut", "Track3Bitrate", 160);
1449 config_set_default_uint(basicConfig, "AdvOut", "Track4Bitrate", 160);
1450 config_set_default_uint(basicConfig, "AdvOut", "Track5Bitrate", 160);
1451 config_set_default_uint(basicConfig, "AdvOut", "Track6Bitrate", 160);
1452
1453 config_set_default_bool(basicConfig, "AdvOut", "RecRB", false);
1454 config_set_default_uint(basicConfig, "AdvOut", "RecRBTime", 20);
1455 config_set_default_int(basicConfig, "AdvOut", "RecRBSize", 512);
1456
1457 config_set_default_uint(basicConfig, "Video", "BaseCX", cx);
1458 config_set_default_uint(basicConfig, "Video", "BaseCY", cy);
1459
1460 /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */
1461 if (!config_has_user_value(basicConfig, "Video", "BaseCX") ||
1462 !config_has_user_value(basicConfig, "Video", "BaseCY")) {
1463 config_set_uint(basicConfig, "Video", "BaseCX", cx);
1464 config_set_uint(basicConfig, "Video", "BaseCY", cy);
1465 config_save_safe(basicConfig, "tmp", nullptr);
1466 }
1467
1468 config_set_default_string(basicConfig, "Output", "FilenameFormatting",
1469 "%CCYY-%MM-%DD %hh-%mm-%ss");
1470
1471 config_set_default_bool(basicConfig, "Output", "DelayEnable", false);
1472 config_set_default_uint(basicConfig, "Output", "DelaySec", 20);
1473 config_set_default_bool(basicConfig, "Output", "DelayPreserve", true);
1474
1475 config_set_default_bool(basicConfig, "Output", "Reconnect", true);
1476 config_set_default_uint(basicConfig, "Output", "RetryDelay", 10);
1477 config_set_default_uint(basicConfig, "Output", "MaxRetries", 20);
1478
1479 config_set_default_string(basicConfig, "Output", "BindIP", "default");
1480 config_set_default_bool(basicConfig, "Output", "NewSocketLoopEnable",
1481 false);
1482 config_set_default_bool(basicConfig, "Output", "LowLatencyEnable",
1483 false);
1484
1485 int i = 0;
1486 uint32_t scale_cx = cx;
1487 uint32_t scale_cy = cy;
1488
1489 /* use a default scaled resolution that has a pixel count no higher
1490 * than 1280x720 */
1491 while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) {
1492 double scale = scaled_vals[i++];
1493 scale_cx = uint32_t(double(cx) / scale);
1494 scale_cy = uint32_t(double(cy) / scale);
1495 }
1496
1497 config_set_default_uint(basicConfig, "Video", "OutputCX", scale_cx);
1498 config_set_default_uint(basicConfig, "Video", "OutputCY", scale_cy);
1499
1500 /* don't allow OutputCX/OutputCY to be susceptible to defaults
1501 * changing */
1502 if (!config_has_user_value(basicConfig, "Video", "OutputCX") ||
1503 !config_has_user_value(basicConfig, "Video", "OutputCY")) {
1504 config_set_uint(basicConfig, "Video", "OutputCX", scale_cx);
1505 config_set_uint(basicConfig, "Video", "OutputCY", scale_cy);
1506 config_save_safe(basicConfig, "tmp", nullptr);
1507 }
1508
1509 config_set_default_uint(basicConfig, "Video", "FPSType", 0);
1510 config_set_default_string(basicConfig, "Video", "FPSCommon", "30");
1511 config_set_default_uint(basicConfig, "Video", "FPSInt", 30);
1512 config_set_default_uint(basicConfig, "Video", "FPSNum", 30);
1513 config_set_default_uint(basicConfig, "Video", "FPSDen", 1);
1514 config_set_default_string(basicConfig, "Video", "ScaleType", "bicubic");
1515 config_set_default_string(basicConfig, "Video", "ColorFormat", "NV12");
1516 config_set_default_string(basicConfig, "Video", "ColorSpace", "709");
1517 config_set_default_string(basicConfig, "Video", "ColorRange",
1518 "Partial");
1519
1520 config_set_default_string(basicConfig, "Audio", "MonitoringDeviceId",
1521 "default");
1522 config_set_default_string(
1523 basicConfig, "Audio", "MonitoringDeviceName",
1524 Str("Basic.Settings.Advanced.Audio.MonitoringDevice"
1525 ".Default"));
1526 config_set_default_uint(basicConfig, "Audio", "SampleRate", 48000);
1527 config_set_default_string(basicConfig, "Audio", "ChannelSetup",
1528 "Stereo");
1529 config_set_default_double(basicConfig, "Audio", "MeterDecayRate",
1530 VOLUME_METER_DECAY_FAST);
1531 config_set_default_uint(basicConfig, "Audio", "PeakMeterType", 0);
1532
1533 CheckExistingCookieId();
1534
1535 return true;
1536 }
1537
1538 extern bool EncoderAvailable(const char *encoder);
1539
InitBasicConfigDefaults2()1540 void OBSBasic::InitBasicConfigDefaults2()
1541 {
1542 bool oldEncDefaults = config_get_bool(App()->GlobalConfig(), "General",
1543 "Pre23Defaults");
1544 bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults;
1545
1546 config_set_default_string(basicConfig, "SimpleOutput", "StreamEncoder",
1547 useNV ? SIMPLE_ENCODER_NVENC
1548 : SIMPLE_ENCODER_X264);
1549 config_set_default_string(basicConfig, "SimpleOutput", "RecEncoder",
1550 useNV ? SIMPLE_ENCODER_NVENC
1551 : SIMPLE_ENCODER_X264);
1552 }
1553
InitBasicConfig()1554 bool OBSBasic::InitBasicConfig()
1555 {
1556 ProfileScope("OBSBasic::InitBasicConfig");
1557
1558 char configPath[512];
1559
1560 int ret = GetProfilePath(configPath, sizeof(configPath), "");
1561 if (ret <= 0) {
1562 OBSErrorBox(nullptr, "Failed to get profile path");
1563 return false;
1564 }
1565
1566 if (os_mkdir(configPath) == MKDIR_ERROR) {
1567 OBSErrorBox(nullptr, "Failed to create profile path");
1568 return false;
1569 }
1570
1571 ret = GetProfilePath(configPath, sizeof(configPath), "basic.ini");
1572 if (ret <= 0) {
1573 OBSErrorBox(nullptr, "Failed to get basic.ini path");
1574 return false;
1575 }
1576
1577 int code = basicConfig.Open(configPath, CONFIG_OPEN_ALWAYS);
1578 if (code != CONFIG_SUCCESS) {
1579 OBSErrorBox(NULL, "Failed to open basic.ini: %d", code);
1580 return false;
1581 }
1582
1583 if (config_get_string(basicConfig, "General", "Name") == nullptr) {
1584 const char *curName = config_get_string(App()->GlobalConfig(),
1585 "Basic", "Profile");
1586
1587 config_set_string(basicConfig, "General", "Name", curName);
1588 basicConfig.SaveSafe("tmp");
1589 }
1590
1591 return InitBasicConfigDefaults();
1592 }
1593
InitOBSCallbacks()1594 void OBSBasic::InitOBSCallbacks()
1595 {
1596 ProfileScope("OBSBasic::InitOBSCallbacks");
1597
1598 signalHandlers.reserve(signalHandlers.size() + 7);
1599 signalHandlers.emplace_back(obs_get_signal_handler(), "source_create",
1600 OBSBasic::SourceCreated, this);
1601 signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove",
1602 OBSBasic::SourceRemoved, this);
1603 signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate",
1604 OBSBasic::SourceActivated, this);
1605 signalHandlers.emplace_back(obs_get_signal_handler(),
1606 "source_deactivate",
1607 OBSBasic::SourceDeactivated, this);
1608 signalHandlers.emplace_back(obs_get_signal_handler(),
1609 "source_audio_activate",
1610 OBSBasic::SourceAudioActivated, this);
1611 signalHandlers.emplace_back(obs_get_signal_handler(),
1612 "source_audio_deactivate",
1613 OBSBasic::SourceAudioDeactivated, this);
1614 signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename",
1615 OBSBasic::SourceRenamed, this);
1616 }
1617
InitPrimitives()1618 void OBSBasic::InitPrimitives()
1619 {
1620 ProfileScope("OBSBasic::InitPrimitives");
1621
1622 obs_enter_graphics();
1623
1624 gs_render_start(true);
1625 gs_vertex2f(0.0f, 0.0f);
1626 gs_vertex2f(0.0f, 1.0f);
1627 gs_vertex2f(1.0f, 0.0f);
1628 gs_vertex2f(1.0f, 1.0f);
1629 box = gs_render_save();
1630
1631 gs_render_start(true);
1632 gs_vertex2f(0.0f, 0.0f);
1633 gs_vertex2f(0.0f, 1.0f);
1634 boxLeft = gs_render_save();
1635
1636 gs_render_start(true);
1637 gs_vertex2f(0.0f, 0.0f);
1638 gs_vertex2f(1.0f, 0.0f);
1639 boxTop = gs_render_save();
1640
1641 gs_render_start(true);
1642 gs_vertex2f(1.0f, 0.0f);
1643 gs_vertex2f(1.0f, 1.0f);
1644 boxRight = gs_render_save();
1645
1646 gs_render_start(true);
1647 gs_vertex2f(0.0f, 1.0f);
1648 gs_vertex2f(1.0f, 1.0f);
1649 boxBottom = gs_render_save();
1650
1651 gs_render_start(true);
1652 for (int i = 0; i <= 360; i += (360 / 20)) {
1653 float pos = RAD(float(i));
1654 gs_vertex2f(cosf(pos), sinf(pos));
1655 }
1656 circle = gs_render_save();
1657
1658 InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin,
1659 &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine);
1660 obs_leave_graphics();
1661 }
1662
ReplayBufferClicked()1663 void OBSBasic::ReplayBufferClicked()
1664 {
1665 if (outputHandler->ReplayBufferActive())
1666 StopReplayBuffer();
1667 else
1668 StartReplayBuffer();
1669 };
1670
AddVCamButton()1671 void OBSBasic::AddVCamButton()
1672 {
1673 vcamButton = new ReplayBufferButton(QTStr("Basic.Main.StartVirtualCam"),
1674 this);
1675 vcamButton->setCheckable(true);
1676 connect(vcamButton.data(), &QPushButton::clicked, this,
1677 &OBSBasic::VCamButtonClicked);
1678
1679 vcamButton->setProperty("themeID", "vcamButton");
1680 ui->buttonsVLayout->insertWidget(2, vcamButton);
1681 setTabOrder(ui->recordButton, vcamButton);
1682 setTabOrder(vcamButton, ui->modeSwitch);
1683 }
1684
ResetOutputs()1685 void OBSBasic::ResetOutputs()
1686 {
1687 ProfileScope("OBSBasic::ResetOutputs");
1688
1689 const char *mode = config_get_string(basicConfig, "Output", "Mode");
1690 bool advOut = astrcmpi(mode, "Advanced") == 0;
1691
1692 if (!outputHandler || !outputHandler->Active()) {
1693 outputHandler.reset();
1694 outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this)
1695 : CreateSimpleOutputHandler(this));
1696
1697 delete replayBufferButton;
1698 delete replayLayout;
1699
1700 if (outputHandler->replayBuffer) {
1701 replayBufferButton = new ReplayBufferButton(
1702 QTStr("Basic.Main.StartReplayBuffer"), this);
1703 replayBufferButton->setCheckable(true);
1704 connect(replayBufferButton.data(),
1705 &QPushButton::clicked, this,
1706 &OBSBasic::ReplayBufferClicked);
1707
1708 replayBufferButton->setSizePolicy(QSizePolicy::Ignored,
1709 QSizePolicy::Fixed);
1710
1711 replayLayout = new QHBoxLayout(this);
1712 replayLayout->addWidget(replayBufferButton);
1713
1714 replayBufferButton->setProperty("themeID",
1715 "replayBufferButton");
1716 ui->buttonsVLayout->insertLayout(2, replayLayout);
1717 setTabOrder(ui->recordButton, replayBufferButton);
1718 setTabOrder(replayBufferButton,
1719 ui->buttonsVLayout->itemAt(3)->widget());
1720 }
1721
1722 if (sysTrayReplayBuffer)
1723 sysTrayReplayBuffer->setEnabled(
1724 !!outputHandler->replayBuffer);
1725 } else {
1726 outputHandler->Update();
1727 }
1728 }
1729
1730 static void AddProjectorMenuMonitors(QMenu *parent, QObject *target,
1731 const char *slot);
1732
1733 #define STARTUP_SEPARATOR \
1734 "==== Startup complete ==============================================="
1735 #define SHUTDOWN_SEPARATOR \
1736 "==== Shutting down =================================================="
1737
1738 #define UNSUPPORTED_ERROR \
1739 "Failed to initialize video:\n\nRequired graphics API functionality " \
1740 "not found. Your GPU may not be supported."
1741
1742 #define UNKNOWN_ERROR \
1743 "Failed to initialize video. Your GPU may not be supported, " \
1744 "or your graphics drivers may need to be updated."
1745
OBSInit()1746 void OBSBasic::OBSInit()
1747 {
1748 ProfileScope("OBSBasic::OBSInit");
1749
1750 const char *sceneCollection = config_get_string(
1751 App()->GlobalConfig(), "Basic", "SceneCollectionFile");
1752 char savePath[1024];
1753 char fileName[1024];
1754 int ret;
1755
1756 if (!sceneCollection)
1757 throw "Failed to get scene collection name";
1758
1759 ret = snprintf(fileName, sizeof(fileName),
1760 "obs-studio/basic/scenes/%s.json", sceneCollection);
1761 if (ret <= 0)
1762 throw "Failed to create scene collection file name";
1763
1764 ret = GetConfigPath(savePath, sizeof(savePath), fileName);
1765 if (ret <= 0)
1766 throw "Failed to get scene collection json file path";
1767
1768 if (!InitBasicConfig())
1769 throw "Failed to load basic.ini";
1770 if (!ResetAudio())
1771 throw "Failed to initialize audio";
1772
1773 ret = ResetVideo();
1774
1775 switch (ret) {
1776 case OBS_VIDEO_MODULE_NOT_FOUND:
1777 throw "Failed to initialize video: Graphics module not found";
1778 case OBS_VIDEO_NOT_SUPPORTED:
1779 throw UNSUPPORTED_ERROR;
1780 case OBS_VIDEO_INVALID_PARAM:
1781 throw "Failed to initialize video: Invalid parameters";
1782 default:
1783 if (ret != OBS_VIDEO_SUCCESS)
1784 throw UNKNOWN_ERROR;
1785 }
1786
1787 /* load audio monitoring */
1788 #if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO
1789 const char *device_name =
1790 config_get_string(basicConfig, "Audio", "MonitoringDeviceName");
1791 const char *device_id =
1792 config_get_string(basicConfig, "Audio", "MonitoringDeviceId");
1793
1794 obs_set_audio_monitoring_device(device_name, device_id);
1795
1796 blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s",
1797 device_name, device_id);
1798 #endif
1799
1800 InitOBSCallbacks();
1801 InitHotkeys();
1802
1803 /* hack to prevent elgato from loading its own QtNetwork that it tries
1804 * to ship with */
1805 #if defined(_WIN32) && !defined(_DEBUG)
1806 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1807 LoadLibraryW(L"Qt5Network");
1808 #else
1809 LoadLibraryW(L"Qt6Network");
1810 #endif
1811 #endif
1812
1813 AddExtraModulePaths();
1814 blog(LOG_INFO, "---------------------------------");
1815 obs_load_all_modules();
1816 blog(LOG_INFO, "---------------------------------");
1817 obs_log_loaded_modules();
1818 blog(LOG_INFO, "---------------------------------");
1819 obs_post_load_modules();
1820
1821 #ifdef BROWSER_AVAILABLE
1822 cef = obs_browser_init_panel();
1823 #endif
1824
1825 obs_data_t *obsData = obs_get_private_data();
1826 vcamEnabled = obs_data_get_bool(obsData, "vcamEnabled");
1827 if (vcamEnabled) {
1828 AddVCamButton();
1829 }
1830 obs_data_release(obsData);
1831
1832 InitBasicConfigDefaults2();
1833
1834 CheckForSimpleModeX264Fallback();
1835
1836 blog(LOG_INFO, STARTUP_SEPARATOR);
1837
1838 ResetOutputs();
1839 CreateHotkeys();
1840
1841 if (!InitService())
1842 throw "Failed to initialize service";
1843
1844 InitPrimitives();
1845
1846 sceneDuplicationMode = config_get_bool(
1847 App()->GlobalConfig(), "BasicWindow", "SceneDuplicationMode");
1848 swapScenesMode = config_get_bool(App()->GlobalConfig(), "BasicWindow",
1849 "SwapScenesMode");
1850 editPropertiesMode = config_get_bool(
1851 App()->GlobalConfig(), "BasicWindow", "EditPropertiesMode");
1852
1853 if (!opt_studio_mode) {
1854 SetPreviewProgramMode(config_get_bool(App()->GlobalConfig(),
1855 "BasicWindow",
1856 "PreviewProgramMode"));
1857 } else {
1858 SetPreviewProgramMode(true);
1859 opt_studio_mode = false;
1860 }
1861
1862 #define SET_VISIBILITY(name, control) \
1863 do { \
1864 if (config_has_user_value(App()->GlobalConfig(), \
1865 "BasicWindow", name)) { \
1866 bool visible = config_get_bool(App()->GlobalConfig(), \
1867 "BasicWindow", name); \
1868 ui->control->setChecked(visible); \
1869 } \
1870 } while (false)
1871
1872 SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars);
1873 SET_VISIBILITY("ShowStatusBar", toggleStatusBar);
1874 #undef SET_VISIBILITY
1875
1876 bool sourceIconsVisible = config_get_bool(
1877 GetGlobalConfig(), "BasicWindow", "ShowSourceIcons");
1878 ui->toggleSourceIcons->setChecked(sourceIconsVisible);
1879
1880 bool contextVisible = config_get_bool(
1881 App()->GlobalConfig(), "BasicWindow", "ShowContextToolbars");
1882 ui->toggleContextBar->setChecked(contextVisible);
1883 ui->contextContainer->setVisible(contextVisible);
1884 if (contextVisible)
1885 UpdateContextBar(true);
1886
1887 {
1888 ProfileScope("OBSBasic::Load");
1889 disableSaving--;
1890 Load(savePath);
1891 disableSaving++;
1892 }
1893
1894 TimedCheckForUpdates();
1895 loaded = true;
1896
1897 previewEnabled = config_get_bool(App()->GlobalConfig(), "BasicWindow",
1898 "PreviewEnabled");
1899
1900 if (!previewEnabled && !IsPreviewProgramMode())
1901 QMetaObject::invokeMethod(this, "EnablePreviewDisplay",
1902 Qt::QueuedConnection,
1903 Q_ARG(bool, previewEnabled));
1904
1905 #ifdef _WIN32
1906 uint32_t winVer = GetWindowsVersion();
1907 if (winVer > 0 && winVer < 0x602) {
1908 bool disableAero =
1909 config_get_bool(basicConfig, "Video", "DisableAero");
1910 SetAeroEnabled(!disableAero);
1911 }
1912 #endif
1913
1914 RefreshSceneCollections();
1915 RefreshProfiles();
1916 disableSaving--;
1917
1918 auto addDisplay = [this](OBSQTDisplay *window) {
1919 obs_display_add_draw_callback(window->GetDisplay(),
1920 OBSBasic::RenderMain, this);
1921
1922 struct obs_video_info ovi;
1923 if (obs_get_video_info(&ovi))
1924 ResizePreview(ovi.base_width, ovi.base_height);
1925 };
1926
1927 connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay);
1928
1929 #ifdef _WIN32
1930 SetWin32DropStyle(this);
1931 show();
1932 #endif
1933
1934 bool alwaysOnTop = config_get_bool(App()->GlobalConfig(), "BasicWindow",
1935 "AlwaysOnTop");
1936
1937 #ifdef ENABLE_WAYLAND
1938 bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND;
1939 #else
1940 bool isWayland = false;
1941 #endif
1942
1943 if (!isWayland && (alwaysOnTop || opt_always_on_top)) {
1944 SetAlwaysOnTop(this, true);
1945 ui->actionAlwaysOnTop->setChecked(true);
1946 } else if (isWayland) {
1947 if (opt_always_on_top)
1948 blog(LOG_INFO,
1949 "Always On Top not available on Wayland, ignoring.");
1950 ui->actionAlwaysOnTop->setEnabled(false);
1951 ui->actionAlwaysOnTop->setVisible(false);
1952 }
1953
1954 #ifndef _WIN32
1955 show();
1956 #endif
1957
1958 /* setup stats dock */
1959 OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false);
1960 statsDock->setWidget(statsDlg);
1961
1962 /* ----------------------------- */
1963 /* add custom browser docks */
1964
1965 #ifdef BROWSER_AVAILABLE
1966 if (cef) {
1967 QAction *action = new QAction(QTStr("Basic.MainMenu."
1968 "View.Docks."
1969 "CustomBrowserDocks"),
1970 this);
1971 ui->viewMenuDocks->insertAction(ui->toggleScenes, action);
1972 connect(action, &QAction::triggered, this,
1973 &OBSBasic::ManageExtraBrowserDocks);
1974 ui->viewMenuDocks->insertSeparator(ui->toggleScenes);
1975
1976 LoadExtraBrowserDocks();
1977 }
1978 #endif
1979
1980 const char *dockStateStr = config_get_string(
1981 App()->GlobalConfig(), "BasicWindow", "DockState");
1982
1983 if (!dockStateStr) {
1984 on_resetUI_triggered();
1985 } else {
1986 QByteArray dockState =
1987 QByteArray::fromBase64(QByteArray(dockStateStr));
1988 if (!restoreState(dockState))
1989 on_resetUI_triggered();
1990 }
1991
1992 bool pre23Defaults = config_get_bool(App()->GlobalConfig(), "General",
1993 "Pre23Defaults");
1994 if (pre23Defaults) {
1995 bool resetDockLock23 = config_get_bool(
1996 App()->GlobalConfig(), "General", "ResetDockLock23");
1997 if (!resetDockLock23) {
1998 config_set_bool(App()->GlobalConfig(), "General",
1999 "ResetDockLock23", true);
2000 config_remove_value(App()->GlobalConfig(),
2001 "BasicWindow", "DocksLocked");
2002 config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
2003 }
2004 }
2005
2006 bool docksLocked = config_get_bool(App()->GlobalConfig(), "BasicWindow",
2007 "DocksLocked");
2008 on_lockUI_toggled(docksLocked);
2009 ui->lockUI->blockSignals(true);
2010 ui->lockUI->setChecked(docksLocked);
2011 ui->lockUI->blockSignals(false);
2012
2013 #ifndef __APPLE__
2014 SystemTray(true);
2015 #endif
2016
2017 #ifdef __APPLE__
2018 disableColorSpaceConversion(this);
2019 #endif
2020
2021 bool has_last_version = config_has_user_value(App()->GlobalConfig(),
2022 "General", "LastVersion");
2023 bool first_run =
2024 config_get_bool(App()->GlobalConfig(), "General", "FirstRun");
2025
2026 if (!first_run) {
2027 config_set_bool(App()->GlobalConfig(), "General", "FirstRun",
2028 true);
2029 config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
2030 }
2031
2032 if (!first_run && !has_last_version && !Active())
2033 QMetaObject::invokeMethod(this, "on_autoConfigure_triggered",
2034 Qt::QueuedConnection);
2035
2036 ToggleMixerLayout(config_get_bool(App()->GlobalConfig(), "BasicWindow",
2037 "VerticalVolControl"));
2038
2039 if (config_get_bool(basicConfig, "General", "OpenStatsOnStartup"))
2040 on_stats_triggered();
2041
2042 OBSBasicStats::InitializeValues();
2043
2044 /* ----------------------- */
2045 /* Add multiview menu */
2046
2047 ui->viewMenu->addSeparator();
2048
2049 multiviewProjectorMenu = new QMenu(QTStr("MultiviewProjector"));
2050 ui->viewMenu->addMenu(multiviewProjectorMenu);
2051 AddProjectorMenuMonitors(multiviewProjectorMenu, this,
2052 SLOT(OpenMultiviewProjector()));
2053 connect(ui->viewMenu->menuAction(), &QAction::hovered, this,
2054 &OBSBasic::UpdateMultiviewProjectorMenu);
2055 ui->viewMenu->addAction(QTStr("MultiviewWindowed"), this,
2056 SLOT(OpenMultiviewWindow()));
2057
2058 ui->sources->UpdateIcons();
2059
2060 #if !defined(_WIN32)
2061 delete ui->actionShowCrashLogs;
2062 delete ui->actionUploadLastCrashLog;
2063 delete ui->menuCrashLogs;
2064 ui->actionShowCrashLogs = nullptr;
2065 ui->actionUploadLastCrashLog = nullptr;
2066 ui->menuCrashLogs = nullptr;
2067 #if !defined(__APPLE__)
2068 delete ui->actionCheckForUpdates;
2069 ui->actionCheckForUpdates = nullptr;
2070 #endif
2071 #endif
2072
2073 #ifdef __APPLE__
2074 /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */
2075 delete ui->actionFullscreenInterface;
2076 ui->actionFullscreenInterface = nullptr;
2077 #endif
2078
2079 #if defined(_WIN32) || defined(__APPLE__)
2080 if (App()->IsUpdaterDisabled())
2081 ui->actionCheckForUpdates->setEnabled(false);
2082 #endif
2083
2084 OnFirstLoad();
2085
2086 activateWindow();
2087
2088 #ifdef __APPLE__
2089 QMetaObject::invokeMethod(this, "DeferredSysTrayLoad",
2090 Qt::QueuedConnection, Q_ARG(int, 10));
2091 #endif
2092 }
2093
OnFirstLoad()2094 void OBSBasic::OnFirstLoad()
2095 {
2096 if (api)
2097 api->on_event(OBS_FRONTEND_EVENT_FINISHED_LOADING);
2098
2099 #if defined(BROWSER_AVAILABLE) && defined(_WIN32)
2100 /* Attempt to load init screen if available */
2101 if (cef) {
2102 WhatsNewInfoThread *wnit = new WhatsNewInfoThread();
2103 connect(wnit, &WhatsNewInfoThread::Result, this,
2104 &OBSBasic::ReceivedIntroJson);
2105
2106 introCheckThread.reset(wnit);
2107 introCheckThread->start();
2108 }
2109 #endif
2110
2111 Auth::Load();
2112
2113 bool showLogViewerOnStartup = config_get_bool(
2114 App()->GlobalConfig(), "LogViewer", "ShowLogStartup");
2115
2116 if (showLogViewerOnStartup)
2117 on_actionViewCurrentLog_triggered();
2118 }
2119
DeferredSysTrayLoad(int requeueCount)2120 void OBSBasic::DeferredSysTrayLoad(int requeueCount)
2121 {
2122 if (--requeueCount > 0) {
2123 QMetaObject::invokeMethod(this, "DeferredSysTrayLoad",
2124 Qt::QueuedConnection,
2125 Q_ARG(int, requeueCount));
2126 return;
2127 }
2128
2129 /* Minimizng to tray on initial startup does not work on mac
2130 * unless it is done in the deferred load */
2131 SystemTray(true);
2132 }
2133
2134 /* shows a "what's new" page on startup of new versions using CEF */
ReceivedIntroJson(const QString & text)2135 void OBSBasic::ReceivedIntroJson(const QString &text)
2136 {
2137 #ifdef BROWSER_AVAILABLE
2138 #ifdef _WIN32
2139 if (closing)
2140 return;
2141
2142 std::string err;
2143 Json json = Json::parse(QT_TO_UTF8(text), err);
2144 if (!err.empty())
2145 return;
2146
2147 std::string info_url;
2148 int info_increment = -1;
2149
2150 /* check to see if there's an info page for this version */
2151 const Json::array &items = json.array_items();
2152 for (const Json &item : items) {
2153 const std::string &version = item["version"].string_value();
2154 const std::string &url = item["url"].string_value();
2155 int increment = item["increment"].int_value();
2156 int rc = item["RC"].int_value();
2157
2158 int major = 0;
2159 int minor = 0;
2160
2161 sscanf(version.c_str(), "%d.%d", &major, &minor);
2162 #if OBS_RELEASE_CANDIDATE > 0
2163 if (major == OBS_RELEASE_CANDIDATE_MAJOR &&
2164 minor == OBS_RELEASE_CANDIDATE_MINOR &&
2165 rc == OBS_RELEASE_CANDIDATE) {
2166 #else
2167 if (major == LIBOBS_API_MAJOR_VER &&
2168 minor == LIBOBS_API_MINOR_VER && rc == 0) {
2169 #endif
2170 info_url = url;
2171 info_increment = increment;
2172 }
2173 }
2174
2175 /* this version was not found, or no info for this version */
2176 if (info_increment == -1) {
2177 return;
2178 }
2179
2180 #if OBS_RELEASE_CANDIDATE > 0
2181 uint32_t lastVersion = config_get_int(App()->GlobalConfig(), "General",
2182 "LastRCVersion");
2183 #else
2184 uint32_t lastVersion =
2185 config_get_int(App()->GlobalConfig(), "General", "LastVersion");
2186 #endif
2187
2188 int current_version_increment = -1;
2189
2190 #if OBS_RELEASE_CANDIDATE > 0
2191 if (lastVersion < OBS_RELEASE_CANDIDATE_VER) {
2192 #else
2193 if ((lastVersion & ~0xFFFF) < (LIBOBS_API_VER & ~0xFFFF)) {
2194 #endif
2195 config_set_int(App()->GlobalConfig(), "General",
2196 "InfoIncrement", -1);
2197 } else {
2198 current_version_increment = config_get_int(
2199 App()->GlobalConfig(), "General", "InfoIncrement");
2200 }
2201
2202 if (info_increment <= current_version_increment) {
2203 return;
2204 }
2205
2206 config_set_int(App()->GlobalConfig(), "General", "InfoIncrement",
2207 info_increment);
2208
2209 /* Don't show What's New dialog for new users */
2210 #if !defined(OBS_RELEASE_CANDIDATE) || OBS_RELEASE_CANDIDATE == 0
2211 if (!lastVersion) {
2212 return;
2213 }
2214 #endif
2215 cef->init_browser();
2216
2217 WhatsNewBrowserInitThread *wnbit =
2218 new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str()));
2219
2220 connect(wnbit, &WhatsNewBrowserInitThread::Result, this,
2221 &OBSBasic::ShowWhatsNew);
2222
2223 whatsNewInitThread.reset(wnbit);
2224 whatsNewInitThread->start();
2225
2226 #else
2227 UNUSED_PARAMETER(text);
2228 #endif
2229 #else
2230 UNUSED_PARAMETER(text);
2231 #endif
2232 }
2233
2234 void OBSBasic::ShowWhatsNew(const QString &url)
2235 {
2236 #ifdef BROWSER_AVAILABLE
2237 #ifdef _WIN32
2238 if (closing)
2239 return;
2240
2241 std::string info_url = QT_TO_UTF8(url);
2242
2243 QDialog *dlg = new QDialog(this);
2244 dlg->setAttribute(Qt::WA_DeleteOnClose, true);
2245 dlg->setWindowTitle("What's New");
2246 dlg->resize(700, 600);
2247
2248 Qt::WindowFlags flags = dlg->windowFlags();
2249 Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint;
2250 dlg->setWindowFlags(flags & (~helpFlag));
2251
2252 QCefWidget *cefWidget = cef->create_widget(nullptr, info_url);
2253 if (!cefWidget) {
2254 return;
2255 }
2256
2257 connect(cefWidget, SIGNAL(titleChanged(const QString &)), dlg,
2258 SLOT(setWindowTitle(const QString &)));
2259
2260 QPushButton *close = new QPushButton(QTStr("Close"));
2261 connect(close, &QAbstractButton::clicked, dlg, &QDialog::accept);
2262
2263 QHBoxLayout *bottomLayout = new QHBoxLayout();
2264 bottomLayout->addStretch();
2265 bottomLayout->addWidget(close);
2266 bottomLayout->addStretch();
2267
2268 QVBoxLayout *topLayout = new QVBoxLayout(dlg);
2269 topLayout->addWidget(cefWidget);
2270 topLayout->addLayout(bottomLayout);
2271
2272 dlg->show();
2273 #else
2274 UNUSED_PARAMETER(url);
2275 #endif
2276 #else
2277 UNUSED_PARAMETER(url);
2278 #endif
2279 }
2280
2281 void OBSBasic::UpdateMultiviewProjectorMenu()
2282 {
2283 multiviewProjectorMenu->clear();
2284 AddProjectorMenuMonitors(multiviewProjectorMenu, this,
2285 SLOT(OpenMultiviewProjector()));
2286 }
2287
2288 void OBSBasic::InitHotkeys()
2289 {
2290 ProfileScope("OBSBasic::InitHotkeys");
2291
2292 struct obs_hotkeys_translations t = {};
2293 t.insert = Str("Hotkeys.Insert");
2294 t.del = Str("Hotkeys.Delete");
2295 t.home = Str("Hotkeys.Home");
2296 t.end = Str("Hotkeys.End");
2297 t.page_up = Str("Hotkeys.PageUp");
2298 t.page_down = Str("Hotkeys.PageDown");
2299 t.num_lock = Str("Hotkeys.NumLock");
2300 t.scroll_lock = Str("Hotkeys.ScrollLock");
2301 t.caps_lock = Str("Hotkeys.CapsLock");
2302 t.backspace = Str("Hotkeys.Backspace");
2303 t.tab = Str("Hotkeys.Tab");
2304 t.print = Str("Hotkeys.Print");
2305 t.pause = Str("Hotkeys.Pause");
2306 t.left = Str("Hotkeys.Left");
2307 t.right = Str("Hotkeys.Right");
2308 t.up = Str("Hotkeys.Up");
2309 t.down = Str("Hotkeys.Down");
2310 #ifdef _WIN32
2311 t.meta = Str("Hotkeys.Windows");
2312 #else
2313 t.meta = Str("Hotkeys.Super");
2314 #endif
2315 t.menu = Str("Hotkeys.Menu");
2316 t.space = Str("Hotkeys.Space");
2317 t.numpad_num = Str("Hotkeys.NumpadNum");
2318 t.numpad_multiply = Str("Hotkeys.NumpadMultiply");
2319 t.numpad_divide = Str("Hotkeys.NumpadDivide");
2320 t.numpad_plus = Str("Hotkeys.NumpadAdd");
2321 t.numpad_minus = Str("Hotkeys.NumpadSubtract");
2322 t.numpad_decimal = Str("Hotkeys.NumpadDecimal");
2323 t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum");
2324 t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply");
2325 t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide");
2326 t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd");
2327 t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract");
2328 t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal");
2329 t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual");
2330 t.mouse_num = Str("Hotkeys.MouseButton");
2331 t.escape = Str("Hotkeys.Escape");
2332 obs_hotkeys_set_translations(&t);
2333
2334 obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"),
2335 Str("Push-to-mute"),
2336 Str("Push-to-talk"));
2337
2338 obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"),
2339 Str("SceneItemHide"));
2340
2341 obs_hotkey_enable_callback_rerouting(true);
2342 obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this);
2343 }
2344
2345 void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed)
2346 {
2347 obs_hotkey_trigger_routed_callback(id, pressed);
2348 }
2349
2350 void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed)
2351 {
2352 OBSBasic &basic = *static_cast<OBSBasic *>(data);
2353 QMetaObject::invokeMethod(&basic, "ProcessHotkey",
2354 Q_ARG(obs_hotkey_id, id),
2355 Q_ARG(bool, pressed));
2356 }
2357
2358 void OBSBasic::CreateHotkeys()
2359 {
2360 ProfileScope("OBSBasic::CreateHotkeys");
2361
2362 auto LoadHotkeyData = [&](const char *name) -> OBSData {
2363 const char *info =
2364 config_get_string(basicConfig, "Hotkeys", name);
2365 if (!info)
2366 return {};
2367
2368 obs_data_t *data = obs_data_create_from_json(info);
2369 if (!data)
2370 return {};
2371
2372 OBSData res = data;
2373 obs_data_release(data);
2374 return res;
2375 };
2376
2377 auto LoadHotkey = [&](obs_hotkey_id id, const char *name) {
2378 obs_data_array_t *array =
2379 obs_data_get_array(LoadHotkeyData(name), "bindings");
2380
2381 obs_hotkey_load(id, array);
2382 obs_data_array_release(array);
2383 };
2384
2385 auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0,
2386 const char *name1) {
2387 obs_data_array_t *array0 =
2388 obs_data_get_array(LoadHotkeyData(name0), "bindings");
2389 obs_data_array_t *array1 =
2390 obs_data_get_array(LoadHotkeyData(name1), "bindings");
2391
2392 obs_hotkey_pair_load(id, array0, array1);
2393 obs_data_array_release(array0);
2394 obs_data_array_release(array1);
2395 };
2396
2397 #define MAKE_CALLBACK(pred, method, log_action) \
2398 [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \
2399 OBSBasic &basic = *static_cast<OBSBasic *>(data); \
2400 if ((pred) && pressed) { \
2401 blog(LOG_INFO, log_action " due to hotkey"); \
2402 method(); \
2403 return true; \
2404 } \
2405 return false; \
2406 }
2407
2408 streamingHotkeys = obs_hotkey_pair_register_frontend(
2409 "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"),
2410 "OBSBasic.StopStreaming", Str("Basic.Main.StopStreaming"),
2411 MAKE_CALLBACK(!basic.outputHandler->StreamingActive() &&
2412 basic.ui->streamButton->isEnabled(),
2413 basic.StartStreaming, "Starting stream"),
2414 MAKE_CALLBACK(basic.outputHandler->StreamingActive() &&
2415 basic.ui->streamButton->isEnabled(),
2416 basic.StopStreaming, "Stopping stream"),
2417 this, this);
2418 LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming",
2419 "OBSBasic.StopStreaming");
2420
2421 auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) {
2422 OBSBasic &basic = *static_cast<OBSBasic *>(data);
2423 if (basic.outputHandler->StreamingActive() && pressed) {
2424 basic.ForceStopStreaming();
2425 }
2426 };
2427
2428 forceStreamingStopHotkey = obs_hotkey_register_frontend(
2429 "OBSBasic.ForceStopStreaming",
2430 Str("Basic.Main.ForceStopStreaming"), cb, this);
2431 LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming");
2432
2433 recordingHotkeys = obs_hotkey_pair_register_frontend(
2434 "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"),
2435 "OBSBasic.StopRecording", Str("Basic.Main.StopRecording"),
2436 MAKE_CALLBACK(!basic.outputHandler->RecordingActive() &&
2437 !basic.ui->recordButton->isChecked(),
2438 basic.StartRecording, "Starting recording"),
2439 MAKE_CALLBACK(basic.outputHandler->RecordingActive() &&
2440 basic.ui->recordButton->isChecked(),
2441 basic.StopRecording, "Stopping recording"),
2442 this, this);
2443 LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording",
2444 "OBSBasic.StopRecording");
2445
2446 pauseHotkeys = obs_hotkey_pair_register_frontend(
2447 "OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"),
2448 "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"),
2449 MAKE_CALLBACK(basic.pause && !basic.pause->isChecked(),
2450 basic.PauseRecording, "Pausing recording"),
2451 MAKE_CALLBACK(basic.pause && basic.pause->isChecked(),
2452 basic.UnpauseRecording, "Unpausing recording"),
2453 this, this);
2454 LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording",
2455 "OBSBasic.UnpauseRecording");
2456
2457 replayBufHotkeys = obs_hotkey_pair_register_frontend(
2458 "OBSBasic.StartReplayBuffer",
2459 Str("Basic.Main.StartReplayBuffer"),
2460 "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"),
2461 MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(),
2462 basic.StartReplayBuffer,
2463 "Starting replay buffer"),
2464 MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(),
2465 basic.StopReplayBuffer, "Stopping replay buffer"),
2466 this, this);
2467 LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer",
2468 "OBSBasic.StopReplayBuffer");
2469
2470 if (vcamEnabled) {
2471 vcamHotkeys = obs_hotkey_pair_register_frontend(
2472 "OBSBasic.StartVirtualCam",
2473 Str("Basic.Main.StartVirtualCam"),
2474 "OBSBasic.StopVirtualCam",
2475 Str("Basic.Main.StopVirtualCam"),
2476 MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(),
2477 basic.StartVirtualCam,
2478 "Starting virtual camera"),
2479 MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(),
2480 basic.StopVirtualCam,
2481 "Stopping virtual camera"),
2482 this, this);
2483 LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam",
2484 "OBSBasic.StopVirtualCam");
2485 }
2486
2487 togglePreviewHotkeys = obs_hotkey_pair_register_frontend(
2488 "OBSBasic.EnablePreview",
2489 Str("Basic.Main.PreviewConextMenu.Enable"),
2490 "OBSBasic.DisablePreview", Str("Basic.Main.Preview.Disable"),
2491 MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview,
2492 "Enabling preview"),
2493 MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview,
2494 "Disabling preview"),
2495 this, this);
2496 LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview",
2497 "OBSBasic.DisablePreview");
2498
2499 contextBarHotkeys = obs_hotkey_pair_register_frontend(
2500 "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"),
2501 "OBSBasic.HideContextBar", Str("Basic.Main.HideContextBar"),
2502 MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(),
2503 basic.ShowContextBar, "Showing Context Bar"),
2504 MAKE_CALLBACK(basic.ui->contextContainer->isVisible(),
2505 basic.HideContextBar, "Hiding Context Bar"),
2506 this, this);
2507 LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar",
2508 "OBSBasic.HideContextBar");
2509 #undef MAKE_CALLBACK
2510
2511 auto togglePreviewProgram = [](void *data, obs_hotkey_id,
2512 obs_hotkey_t *, bool pressed) {
2513 if (pressed)
2514 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
2515 "on_modeSwitch_clicked",
2516 Qt::QueuedConnection);
2517 };
2518
2519 togglePreviewProgramHotkey = obs_hotkey_register_frontend(
2520 "OBSBasic.TogglePreviewProgram",
2521 Str("Basic.TogglePreviewProgramMode"), togglePreviewProgram,
2522 this);
2523 LoadHotkey(togglePreviewProgramHotkey, "OBSBasic.TogglePreviewProgram");
2524
2525 auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *,
2526 bool pressed) {
2527 if (pressed)
2528 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
2529 "TransitionClicked",
2530 Qt::QueuedConnection);
2531 };
2532
2533 transitionHotkey = obs_hotkey_register_frontend(
2534 "OBSBasic.Transition", Str("Transition"), transition, this);
2535 LoadHotkey(transitionHotkey, "OBSBasic.Transition");
2536
2537 auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *,
2538 bool pressed) {
2539 if (pressed)
2540 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
2541 "ResetStatsHotkey",
2542 Qt::QueuedConnection);
2543 };
2544
2545 statsHotkey = obs_hotkey_register_frontend(
2546 "OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"),
2547 resetStats, this);
2548 LoadHotkey(statsHotkey, "OBSBasic.ResetStats");
2549
2550 auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *,
2551 bool pressed) {
2552 if (pressed)
2553 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
2554 "Screenshot",
2555 Qt::QueuedConnection);
2556 };
2557
2558 screenshotHotkey = obs_hotkey_register_frontend(
2559 "OBSBasic.Screenshot", Str("Screenshot"), screenshot, this);
2560 LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot");
2561
2562 auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *,
2563 bool pressed) {
2564 if (pressed)
2565 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
2566 "ScreenshotSelectedSource",
2567 Qt::QueuedConnection);
2568 };
2569
2570 sourceScreenshotHotkey = obs_hotkey_register_frontend(
2571 "OBSBasic.SelectedSourceScreenshot",
2572 Str("Screenshot.SourceHotkey"), screenshotSource, this);
2573 LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot");
2574 }
2575
2576 void OBSBasic::ClearHotkeys()
2577 {
2578 obs_hotkey_pair_unregister(streamingHotkeys);
2579 obs_hotkey_pair_unregister(recordingHotkeys);
2580 obs_hotkey_pair_unregister(pauseHotkeys);
2581 obs_hotkey_pair_unregister(replayBufHotkeys);
2582 obs_hotkey_pair_unregister(vcamHotkeys);
2583 obs_hotkey_pair_unregister(togglePreviewHotkeys);
2584 obs_hotkey_pair_unregister(contextBarHotkeys);
2585 obs_hotkey_unregister(forceStreamingStopHotkey);
2586 obs_hotkey_unregister(togglePreviewProgramHotkey);
2587 obs_hotkey_unregister(transitionHotkey);
2588 obs_hotkey_unregister(statsHotkey);
2589 obs_hotkey_unregister(screenshotHotkey);
2590 obs_hotkey_unregister(sourceScreenshotHotkey);
2591 }
2592
2593 OBSBasic::~OBSBasic()
2594 {
2595 /* clear out UI event queue */
2596 QApplication::sendPostedEvents(App());
2597
2598 if (updateCheckThread && updateCheckThread->isRunning())
2599 updateCheckThread->wait();
2600
2601 delete screenshotData;
2602 delete multiviewProjectorMenu;
2603 delete previewProjector;
2604 delete studioProgramProjector;
2605 delete previewProjectorSource;
2606 delete previewProjectorMain;
2607 delete sourceProjector;
2608 delete sceneProjectorMenu;
2609 delete scaleFilteringMenu;
2610 delete colorMenu;
2611 delete colorWidgetAction;
2612 delete colorSelect;
2613 delete deinterlaceMenu;
2614 delete perSceneTransitionMenu;
2615 delete shortcutFilter;
2616 delete trayMenu;
2617 delete programOptions;
2618 delete program;
2619
2620 /* XXX: any obs data must be released before calling obs_shutdown.
2621 * currently, we can't automate this with C++ RAII because of the
2622 * delicate nature of obs_shutdown needing to be freed before the UI
2623 * can be freed, and we have no control over the destruction order of
2624 * the Qt UI stuff, so we have to manually clear any references to
2625 * libobs. */
2626 delete cpuUsageTimer;
2627 os_cpu_usage_info_destroy(cpuUsageInfo);
2628
2629 obs_hotkey_set_callback_routing_func(nullptr, nullptr);
2630 ClearHotkeys();
2631
2632 service = nullptr;
2633 outputHandler.reset();
2634
2635 if (interaction)
2636 delete interaction;
2637
2638 if (properties)
2639 delete properties;
2640
2641 if (filters)
2642 delete filters;
2643
2644 if (transformWindow)
2645 delete transformWindow;
2646
2647 if (advAudioWindow)
2648 delete advAudioWindow;
2649
2650 if (about)
2651 delete about;
2652
2653 obs_display_remove_draw_callback(ui->preview->GetDisplay(),
2654 OBSBasic::RenderMain, this);
2655
2656 obs_enter_graphics();
2657 gs_vertexbuffer_destroy(box);
2658 gs_vertexbuffer_destroy(boxLeft);
2659 gs_vertexbuffer_destroy(boxTop);
2660 gs_vertexbuffer_destroy(boxRight);
2661 gs_vertexbuffer_destroy(boxBottom);
2662 gs_vertexbuffer_destroy(circle);
2663 gs_vertexbuffer_destroy(actionSafeMargin);
2664 gs_vertexbuffer_destroy(graphicsSafeMargin);
2665 gs_vertexbuffer_destroy(fourByThreeSafeMargin);
2666 gs_vertexbuffer_destroy(leftLine);
2667 gs_vertexbuffer_destroy(topLine);
2668 gs_vertexbuffer_destroy(rightLine);
2669 obs_leave_graphics();
2670
2671 /* When shutting down, sometimes source references can get in to the
2672 * event queue, and if we don't forcibly process those events they
2673 * won't get processed until after obs_shutdown has been called. I
2674 * really wish there were a more elegant way to deal with this via C++,
2675 * but Qt doesn't use C++ in a normal way, so you can't really rely on
2676 * normal C++ behavior for your data to be freed in the order that you
2677 * expect or want it to. */
2678 QApplication::sendPostedEvents(this);
2679
2680 config_set_int(App()->GlobalConfig(), "General", "LastVersion",
2681 LIBOBS_API_VER);
2682 #if OBS_RELEASE_CANDIDATE > 0
2683 config_set_int(App()->GlobalConfig(), "General", "LastRCVersion",
2684 OBS_RELEASE_CANDIDATE_VER);
2685 #endif
2686
2687 bool alwaysOnTop = IsAlwaysOnTop(this);
2688
2689 config_set_bool(App()->GlobalConfig(), "BasicWindow", "PreviewEnabled",
2690 previewEnabled);
2691 config_set_bool(App()->GlobalConfig(), "BasicWindow", "AlwaysOnTop",
2692 alwaysOnTop);
2693 config_set_bool(App()->GlobalConfig(), "BasicWindow",
2694 "SceneDuplicationMode", sceneDuplicationMode);
2695 config_set_bool(App()->GlobalConfig(), "BasicWindow", "SwapScenesMode",
2696 swapScenesMode);
2697 config_set_bool(App()->GlobalConfig(), "BasicWindow",
2698 "EditPropertiesMode", editPropertiesMode);
2699 config_set_bool(App()->GlobalConfig(), "BasicWindow",
2700 "PreviewProgramMode", IsPreviewProgramMode());
2701 config_set_bool(App()->GlobalConfig(), "BasicWindow", "DocksLocked",
2702 ui->lockUI->isChecked());
2703 config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
2704
2705 #ifdef _WIN32
2706 uint32_t winVer = GetWindowsVersion();
2707 if (winVer > 0 && winVer < 0x602) {
2708 bool disableAero =
2709 config_get_bool(basicConfig, "Video", "DisableAero");
2710 if (disableAero) {
2711 SetAeroEnabled(true);
2712 }
2713 }
2714 #endif
2715
2716 #ifdef BROWSER_AVAILABLE
2717 DestroyPanelCookieManager();
2718 delete cef;
2719 cef = nullptr;
2720 #endif
2721 }
2722
2723 void OBSBasic::SaveProjectNow()
2724 {
2725 if (disableSaving)
2726 return;
2727
2728 projectChanged = true;
2729 SaveProjectDeferred();
2730 }
2731
2732 void OBSBasic::SaveProject()
2733 {
2734 if (disableSaving)
2735 return;
2736
2737 projectChanged = true;
2738 QMetaObject::invokeMethod(this, "SaveProjectDeferred",
2739 Qt::QueuedConnection);
2740 }
2741
2742 void OBSBasic::SaveProjectDeferred()
2743 {
2744 if (disableSaving)
2745 return;
2746
2747 if (!projectChanged)
2748 return;
2749
2750 projectChanged = false;
2751
2752 const char *sceneCollection = config_get_string(
2753 App()->GlobalConfig(), "Basic", "SceneCollectionFile");
2754
2755 char savePath[1024];
2756 char fileName[1024];
2757 int ret;
2758
2759 if (!sceneCollection)
2760 return;
2761
2762 ret = snprintf(fileName, sizeof(fileName),
2763 "obs-studio/basic/scenes/%s.json", sceneCollection);
2764 if (ret <= 0)
2765 return;
2766
2767 ret = GetConfigPath(savePath, sizeof(savePath), fileName);
2768 if (ret <= 0)
2769 return;
2770
2771 Save(savePath);
2772 }
2773
2774 OBSSource OBSBasic::GetProgramSource()
2775 {
2776 return OBSGetStrongRef(programScene);
2777 }
2778
2779 OBSScene OBSBasic::GetCurrentScene()
2780 {
2781 QListWidgetItem *item = ui->scenes->currentItem();
2782 return item ? GetOBSRef<OBSScene>(item) : nullptr;
2783 }
2784
2785 OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item)
2786 {
2787 return item ? GetOBSRef<OBSSceneItem>(item) : nullptr;
2788 }
2789
2790 OBSSceneItem OBSBasic::GetCurrentSceneItem()
2791 {
2792 return ui->sources->Get(GetTopSelectedSourceItem());
2793 }
2794
2795 void OBSBasic::UpdatePreviewScalingMenu()
2796 {
2797 bool fixedScaling = ui->preview->IsFixedScaling();
2798 float scalingAmount = ui->preview->GetScalingAmount();
2799 if (!fixedScaling) {
2800 ui->actionScaleWindow->setChecked(true);
2801 ui->actionScaleCanvas->setChecked(false);
2802 ui->actionScaleOutput->setChecked(false);
2803 return;
2804 }
2805
2806 obs_video_info ovi;
2807 obs_get_video_info(&ovi);
2808
2809 ui->actionScaleWindow->setChecked(false);
2810 ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f);
2811 ui->actionScaleOutput->setChecked(scalingAmount ==
2812 float(ovi.output_width) /
2813 float(ovi.base_width));
2814 }
2815
2816 void OBSBasic::CreateInteractionWindow(obs_source_t *source)
2817 {
2818 bool closed = true;
2819 if (interaction)
2820 closed = interaction->close();
2821
2822 if (!closed)
2823 return;
2824
2825 interaction = new OBSBasicInteraction(this, source);
2826 interaction->Init();
2827 interaction->setAttribute(Qt::WA_DeleteOnClose, true);
2828 }
2829
2830 void OBSBasic::CreatePropertiesWindow(obs_source_t *source)
2831 {
2832 bool closed = true;
2833 if (properties)
2834 closed = properties->close();
2835
2836 if (!closed)
2837 return;
2838
2839 properties = new OBSBasicProperties(this, source);
2840 properties->Init();
2841 properties->setAttribute(Qt::WA_DeleteOnClose, true);
2842 }
2843
2844 void OBSBasic::CreateFiltersWindow(obs_source_t *source)
2845 {
2846 bool closed = true;
2847 if (filters)
2848 closed = filters->close();
2849
2850 if (!closed)
2851 return;
2852
2853 filters = new OBSBasicFilters(this, source);
2854 filters->Init();
2855 filters->setAttribute(Qt::WA_DeleteOnClose, true);
2856 }
2857
2858 /* Qt callbacks for invokeMethod */
2859
2860 void OBSBasic::AddScene(OBSSource source)
2861 {
2862 const char *name = obs_source_get_name(source);
2863 obs_scene_t *scene = obs_scene_from_source(source);
2864
2865 QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name));
2866 SetOBSRef(item, OBSScene(scene));
2867 ui->scenes->addItem(item);
2868
2869 obs_hotkey_register_source(
2870 source, "OBSBasic.SelectScene",
2871 Str("Basic.Hotkeys.SelectScene"),
2872 [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) {
2873 OBSBasic *main = reinterpret_cast<OBSBasic *>(
2874 App()->GetMainWindow());
2875
2876 auto potential_source =
2877 static_cast<obs_source_t *>(data);
2878 auto source = obs_source_get_ref(potential_source);
2879 if (source && pressed)
2880 main->SetCurrentScene(source);
2881 obs_source_release(source);
2882 },
2883 static_cast<obs_source_t *>(source));
2884
2885 signal_handler_t *handler = obs_source_get_signal_handler(source);
2886
2887 SignalContainer<OBSScene> container;
2888 container.ref = scene;
2889 container.handlers.assign({
2890 std::make_shared<OBSSignal>(handler, "item_add",
2891 OBSBasic::SceneItemAdded, this),
2892 std::make_shared<OBSSignal>(handler, "reorder",
2893 OBSBasic::SceneReordered, this),
2894 std::make_shared<OBSSignal>(handler, "refresh",
2895 OBSBasic::SceneRefreshed, this),
2896 });
2897
2898 item->setData(static_cast<int>(QtDataRole::OBSSignals),
2899 QVariant::fromValue(container));
2900
2901 /* if the scene already has items (a duplicated scene) add them */
2902 auto addSceneItem = [this](obs_sceneitem_t *item) {
2903 AddSceneItem(item);
2904 };
2905
2906 using addSceneItem_t = decltype(addSceneItem);
2907
2908 obs_scene_enum_items(
2909 scene,
2910 [](obs_scene_t *, obs_sceneitem_t *item, void *param) {
2911 addSceneItem_t *func;
2912 func = reinterpret_cast<addSceneItem_t *>(param);
2913 (*func)(item);
2914 return true;
2915 },
2916 &addSceneItem);
2917
2918 SaveProject();
2919
2920 if (!disableSaving) {
2921 obs_source_t *source = obs_scene_get_source(scene);
2922 blog(LOG_INFO, "User added scene '%s'",
2923 obs_source_get_name(source));
2924
2925 OBSProjector::UpdateMultiviewProjectors();
2926 }
2927
2928 if (api)
2929 api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
2930 }
2931
2932 void OBSBasic::RemoveScene(OBSSource source)
2933 {
2934 obs_scene_t *scene = obs_scene_from_source(source);
2935
2936 QListWidgetItem *sel = nullptr;
2937 int count = ui->scenes->count();
2938
2939 for (int i = 0; i < count; i++) {
2940 auto item = ui->scenes->item(i);
2941 auto cur_scene = GetOBSRef<OBSScene>(item);
2942 if (cur_scene != scene)
2943 continue;
2944
2945 sel = item;
2946 break;
2947 }
2948
2949 if (sel != nullptr) {
2950 if (sel == ui->scenes->currentItem())
2951 ui->sources->Clear();
2952 delete sel;
2953 }
2954
2955 SaveProject();
2956
2957 if (!disableSaving) {
2958 blog(LOG_INFO, "User Removed scene '%s'",
2959 obs_source_get_name(source));
2960
2961 OBSProjector::UpdateMultiviewProjectors();
2962 }
2963
2964 if (api)
2965 api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
2966 }
2967
2968 static bool select_one(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
2969 {
2970 obs_sceneitem_t *selectedItem =
2971 reinterpret_cast<obs_sceneitem_t *>(param);
2972 if (obs_sceneitem_is_group(item))
2973 obs_sceneitem_group_enum_items(item, select_one, param);
2974
2975 obs_sceneitem_select(item, (selectedItem == item));
2976
2977 UNUSED_PARAMETER(scene);
2978 return true;
2979 }
2980
2981 void OBSBasic::AddSceneItem(OBSSceneItem item)
2982 {
2983 obs_scene_t *scene = obs_sceneitem_get_scene(item);
2984
2985 if (GetCurrentScene() == scene)
2986 ui->sources->Add(item);
2987
2988 SaveProject();
2989
2990 if (!disableSaving) {
2991 obs_source_t *sceneSource = obs_scene_get_source(scene);
2992 obs_source_t *itemSource = obs_sceneitem_get_source(item);
2993 blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'",
2994 obs_source_get_name(itemSource),
2995 obs_source_get_id(itemSource),
2996 obs_source_get_name(sceneSource));
2997
2998 obs_scene_enum_items(scene, select_one,
2999 (obs_sceneitem_t *)item);
3000 }
3001 }
3002
3003 static void RenameListValues(QListWidget *listWidget, const QString &newName,
3004 const QString &prevName)
3005 {
3006 QList<QListWidgetItem *> items =
3007 listWidget->findItems(prevName, Qt::MatchExactly);
3008
3009 for (int i = 0; i < items.count(); i++)
3010 items[i]->setText(newName);
3011 }
3012
3013 void OBSBasic::RenameSources(OBSSource source, QString newName,
3014 QString prevName)
3015 {
3016 RenameListValues(ui->scenes, newName, prevName);
3017
3018 for (size_t i = 0; i < volumes.size(); i++) {
3019 if (volumes[i]->GetName().compare(prevName) == 0)
3020 volumes[i]->SetName(newName);
3021 }
3022
3023 for (size_t i = 0; i < projectors.size(); i++) {
3024 if (projectors[i]->GetSource() == source)
3025 projectors[i]->RenameProjector(prevName, newName);
3026 }
3027
3028 SaveProject();
3029
3030 obs_scene_t *scene = obs_scene_from_source(source);
3031 if (scene)
3032 OBSProjector::UpdateMultiviewProjectors();
3033
3034 UpdateContextBar();
3035 }
3036
3037 void OBSBasic::ClearContextBar()
3038 {
3039 QLayoutItem *la = ui->emptySpace->layout()->itemAt(0);
3040 if (la) {
3041 delete la->widget();
3042 ui->emptySpace->layout()->removeItem(la);
3043 }
3044 }
3045
3046 static bool is_network_media_source(obs_source_t *source, const char *id)
3047 {
3048 if (strcmp(id, "ffmpeg_source") != 0)
3049 return false;
3050
3051 obs_data_t *s = obs_source_get_settings(source);
3052 bool is_local_file = obs_data_get_bool(s, "is_local_file");
3053 obs_data_release(s);
3054
3055 return !is_local_file;
3056 }
3057
3058 void OBSBasic::UpdateContextBarDeferred(bool force)
3059 {
3060 QMetaObject::invokeMethod(this, "UpdateContextBar",
3061 Qt::QueuedConnection, Q_ARG(bool, force));
3062 }
3063
3064 void OBSBasic::UpdateContextBar(bool force)
3065 {
3066 if (!ui->contextContainer->isVisible() && !force)
3067 return;
3068
3069 OBSSceneItem item = GetCurrentSceneItem();
3070
3071 ClearContextBar();
3072
3073 if (item) {
3074 obs_source_t *source = obs_sceneitem_get_source(item);
3075 const char *id = obs_source_get_unversioned_id(source);
3076 uint32_t flags = obs_source_get_output_flags(source);
3077
3078 ui->sourceInteractButton->setVisible(flags &
3079 OBS_SOURCE_INTERACTION);
3080
3081 if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) {
3082 if (!is_network_media_source(source, id)) {
3083 MediaControls *mediaControls =
3084 new MediaControls(ui->emptySpace);
3085 mediaControls->SetSource(source);
3086
3087 ui->emptySpace->layout()->addWidget(
3088 mediaControls);
3089 }
3090 } else if (strcmp(id, "browser_source") == 0) {
3091 BrowserToolbar *c =
3092 new BrowserToolbar(ui->emptySpace, source);
3093 ui->emptySpace->layout()->addWidget(c);
3094
3095 } else if (strcmp(id, "wasapi_input_capture") == 0 ||
3096 strcmp(id, "wasapi_output_capture") == 0 ||
3097 strcmp(id, "coreaudio_input_capture") == 0 ||
3098 strcmp(id, "coreaudio_output_capture") == 0 ||
3099 strcmp(id, "pulse_input_capture") == 0 ||
3100 strcmp(id, "pulse_output_capture") == 0 ||
3101 strcmp(id, "alsa_input_capture") == 0) {
3102 AudioCaptureToolbar *c =
3103 new AudioCaptureToolbar(ui->emptySpace, source);
3104 c->Init();
3105 ui->emptySpace->layout()->addWidget(c);
3106
3107 } else if (strcmp(id, "window_capture") == 0 ||
3108 strcmp(id, "xcomposite_input") == 0) {
3109 WindowCaptureToolbar *c = new WindowCaptureToolbar(
3110 ui->emptySpace, source);
3111 c->Init();
3112 ui->emptySpace->layout()->addWidget(c);
3113
3114 } else if (strcmp(id, "monitor_capture") == 0 ||
3115 strcmp(id, "display_capture") == 0 ||
3116 strcmp(id, "xshm_input") == 0) {
3117 DisplayCaptureToolbar *c = new DisplayCaptureToolbar(
3118 ui->emptySpace, source);
3119 c->Init();
3120 ui->emptySpace->layout()->addWidget(c);
3121
3122 } else if (strcmp(id, "dshow_input") == 0) {
3123 DeviceCaptureToolbar *c = new DeviceCaptureToolbar(
3124 ui->emptySpace, source);
3125 ui->emptySpace->layout()->addWidget(c);
3126
3127 } else if (strcmp(id, "game_capture") == 0) {
3128 GameCaptureToolbar *c =
3129 new GameCaptureToolbar(ui->emptySpace, source);
3130 ui->emptySpace->layout()->addWidget(c);
3131
3132 } else if (strcmp(id, "image_source") == 0) {
3133 ImageSourceToolbar *c =
3134 new ImageSourceToolbar(ui->emptySpace, source);
3135 ui->emptySpace->layout()->addWidget(c);
3136
3137 } else if (strcmp(id, "color_source") == 0) {
3138 ColorSourceToolbar *c =
3139 new ColorSourceToolbar(ui->emptySpace, source);
3140 ui->emptySpace->layout()->addWidget(c);
3141
3142 } else if (strcmp(id, "text_ft2_source") == 0 ||
3143 strcmp(id, "text_gdiplus") == 0) {
3144 TextSourceToolbar *c =
3145 new TextSourceToolbar(ui->emptySpace, source);
3146 ui->emptySpace->layout()->addWidget(c);
3147 }
3148
3149 QIcon icon;
3150
3151 if (strcmp(id, "scene") == 0)
3152 icon = GetSceneIcon();
3153 else if (strcmp(id, "group") == 0)
3154 icon = GetGroupIcon();
3155 else
3156 icon = GetSourceIcon(id);
3157
3158 QPixmap pixmap = icon.pixmap(QSize(16, 16));
3159 ui->contextSourceIcon->setPixmap(pixmap);
3160 ui->contextSourceIconSpacer->hide();
3161 ui->contextSourceIcon->show();
3162
3163 const char *name = obs_source_get_name(source);
3164 ui->contextSourceLabel->setText(name);
3165
3166 ui->sourceFiltersButton->setEnabled(true);
3167 ui->sourcePropertiesButton->setEnabled(
3168 obs_source_configurable(source));
3169 } else {
3170 ui->contextSourceIcon->hide();
3171 ui->contextSourceIconSpacer->show();
3172 ui->contextSourceLabel->setText(
3173 QTStr("ContextBar.NoSelectedSource"));
3174
3175 ui->sourceFiltersButton->setEnabled(false);
3176 ui->sourcePropertiesButton->setEnabled(false);
3177 ui->sourceInteractButton->setVisible(false);
3178 }
3179 }
3180
3181 static inline bool SourceMixerHidden(obs_source_t *source)
3182 {
3183 obs_data_t *priv_settings = obs_source_get_private_settings(source);
3184 bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden");
3185 obs_data_release(priv_settings);
3186
3187 return hidden;
3188 }
3189
3190 static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden)
3191 {
3192 obs_data_t *priv_settings = obs_source_get_private_settings(source);
3193 obs_data_set_bool(priv_settings, "mixer_hidden", hidden);
3194 obs_data_release(priv_settings);
3195 }
3196
3197 void OBSBasic::GetAudioSourceFilters()
3198 {
3199 QAction *action = reinterpret_cast<QAction *>(sender());
3200 VolControl *vol = action->property("volControl").value<VolControl *>();
3201 obs_source_t *source = vol->GetSource();
3202
3203 CreateFiltersWindow(source);
3204 }
3205
3206 void OBSBasic::GetAudioSourceProperties()
3207 {
3208 QAction *action = reinterpret_cast<QAction *>(sender());
3209 VolControl *vol = action->property("volControl").value<VolControl *>();
3210 obs_source_t *source = vol->GetSource();
3211
3212 CreatePropertiesWindow(source);
3213 }
3214
3215 void OBSBasic::HideAudioControl()
3216 {
3217 QAction *action = reinterpret_cast<QAction *>(sender());
3218 VolControl *vol = action->property("volControl").value<VolControl *>();
3219 obs_source_t *source = vol->GetSource();
3220
3221 if (!SourceMixerHidden(source)) {
3222 SetSourceMixerHidden(source, true);
3223 DeactivateAudioSource(source);
3224 }
3225 }
3226
3227 void OBSBasic::UnhideAllAudioControls()
3228 {
3229 auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */
3230 {
3231 if (!obs_source_active(source))
3232 return true;
3233 if (!SourceMixerHidden(source))
3234 return true;
3235
3236 SetSourceMixerHidden(source, false);
3237 ActivateAudioSource(source);
3238 return true;
3239 };
3240
3241 using UnhideAudioMixer_t = decltype(UnhideAudioMixer);
3242
3243 auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */
3244 { return (*reinterpret_cast<UnhideAudioMixer_t *>(data))(source); };
3245
3246 obs_enum_sources(PreEnum, &UnhideAudioMixer);
3247 }
3248
3249 void OBSBasic::ToggleHideMixer()
3250 {
3251 OBSSceneItem item = GetCurrentSceneItem();
3252 OBSSource source = obs_sceneitem_get_source(item);
3253
3254 if (!SourceMixerHidden(source)) {
3255 SetSourceMixerHidden(source, true);
3256 DeactivateAudioSource(source);
3257 } else {
3258 SetSourceMixerHidden(source, false);
3259 ActivateAudioSource(source);
3260 }
3261 }
3262
3263 void OBSBasic::MixerRenameSource()
3264 {
3265 QAction *action = reinterpret_cast<QAction *>(sender());
3266 VolControl *vol = action->property("volControl").value<VolControl *>();
3267 OBSSource source = vol->GetSource();
3268
3269 const char *prevName = obs_source_get_name(source);
3270
3271 for (;;) {
3272 string name;
3273 bool accepted = NameDialog::AskForName(
3274 this, QTStr("Basic.Main.MixerRename.Title"),
3275 QTStr("Basic.Main.MixerRename.Text"), name,
3276 QT_UTF8(prevName));
3277 if (!accepted)
3278 return;
3279
3280 if (name.empty()) {
3281 OBSMessageBox::warning(this,
3282 QTStr("NoNameEntered.Title"),
3283 QTStr("NoNameEntered.Text"));
3284 continue;
3285 }
3286
3287 OBSSource sourceTest = obs_get_source_by_name(name.c_str());
3288 obs_source_release(sourceTest);
3289
3290 if (sourceTest) {
3291 OBSMessageBox::warning(this, QTStr("NameExists.Title"),
3292 QTStr("NameExists.Text"));
3293 continue;
3294 }
3295
3296 obs_source_set_name(source, name.c_str());
3297 break;
3298 }
3299 }
3300
3301 static inline bool SourceVolumeLocked(obs_source_t *source)
3302 {
3303 obs_data_t *priv_settings = obs_source_get_private_settings(source);
3304 bool lock = obs_data_get_bool(priv_settings, "volume_locked");
3305 obs_data_release(priv_settings);
3306
3307 return lock;
3308 }
3309
3310 void OBSBasic::LockVolumeControl(bool lock)
3311 {
3312 QAction *action = reinterpret_cast<QAction *>(sender());
3313 VolControl *vol = action->property("volControl").value<VolControl *>();
3314 obs_source_t *source = vol->GetSource();
3315
3316 obs_data_t *priv_settings = obs_source_get_private_settings(source);
3317 obs_data_set_bool(priv_settings, "volume_locked", lock);
3318 obs_data_release(priv_settings);
3319
3320 vol->EnableSlider(!lock);
3321 }
3322
3323 void OBSBasic::VolControlContextMenu()
3324 {
3325 VolControl *vol = reinterpret_cast<VolControl *>(sender());
3326
3327 /* ------------------- */
3328
3329 QAction lockAction(QTStr("LockVolume"), this);
3330 lockAction.setCheckable(true);
3331 lockAction.setChecked(SourceVolumeLocked(vol->GetSource()));
3332
3333 QAction hideAction(QTStr("Hide"), this);
3334 QAction unhideAllAction(QTStr("UnhideAll"), this);
3335 QAction mixerRenameAction(QTStr("Rename"), this);
3336
3337 QAction copyFiltersAction(QTStr("Copy.Filters"), this);
3338 QAction pasteFiltersAction(QTStr("Paste.Filters"), this);
3339
3340 QAction filtersAction(QTStr("Filters"), this);
3341 QAction propertiesAction(QTStr("Properties"), this);
3342 QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this);
3343
3344 QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this);
3345 toggleControlLayoutAction.setCheckable(true);
3346 toggleControlLayoutAction.setChecked(config_get_bool(
3347 GetGlobalConfig(), "BasicWindow", "VerticalVolControl"));
3348
3349 /* ------------------- */
3350
3351 connect(&hideAction, &QAction::triggered, this,
3352 &OBSBasic::HideAudioControl, Qt::DirectConnection);
3353 connect(&unhideAllAction, &QAction::triggered, this,
3354 &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection);
3355 connect(&lockAction, &QAction::toggled, this,
3356 &OBSBasic::LockVolumeControl, Qt::DirectConnection);
3357 connect(&mixerRenameAction, &QAction::triggered, this,
3358 &OBSBasic::MixerRenameSource, Qt::DirectConnection);
3359
3360 connect(©FiltersAction, &QAction::triggered, this,
3361 &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection);
3362 connect(&pasteFiltersAction, &QAction::triggered, this,
3363 &OBSBasic::AudioMixerPasteFilters, Qt::DirectConnection);
3364
3365 connect(&filtersAction, &QAction::triggered, this,
3366 &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection);
3367 connect(&propertiesAction, &QAction::triggered, this,
3368 &OBSBasic::GetAudioSourceProperties, Qt::DirectConnection);
3369 connect(&advPropAction, &QAction::triggered, this,
3370 &OBSBasic::on_actionAdvAudioProperties_triggered,
3371 Qt::DirectConnection);
3372
3373 /* ------------------- */
3374
3375 connect(&toggleControlLayoutAction, &QAction::changed, this,
3376 &OBSBasic::ToggleVolControlLayout, Qt::DirectConnection);
3377
3378 /* ------------------- */
3379
3380 hideAction.setProperty("volControl",
3381 QVariant::fromValue<VolControl *>(vol));
3382 lockAction.setProperty("volControl",
3383 QVariant::fromValue<VolControl *>(vol));
3384 mixerRenameAction.setProperty("volControl",
3385 QVariant::fromValue<VolControl *>(vol));
3386
3387 copyFiltersAction.setProperty("volControl",
3388 QVariant::fromValue<VolControl *>(vol));
3389 pasteFiltersAction.setProperty("volControl",
3390 QVariant::fromValue<VolControl *>(vol));
3391
3392 filtersAction.setProperty("volControl",
3393 QVariant::fromValue<VolControl *>(vol));
3394 propertiesAction.setProperty("volControl",
3395 QVariant::fromValue<VolControl *>(vol));
3396
3397 /* ------------------- */
3398
3399 copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) >
3400 0);
3401
3402 if (copyFiltersString == nullptr)
3403 pasteFiltersAction.setEnabled(false);
3404 else
3405 pasteFiltersAction.setEnabled(true);
3406
3407 QMenu popup;
3408 vol->SetContextMenu(&popup);
3409 popup.addAction(&lockAction);
3410 popup.addSeparator();
3411 popup.addAction(&unhideAllAction);
3412 popup.addAction(&hideAction);
3413 popup.addAction(&mixerRenameAction);
3414 popup.addSeparator();
3415 popup.addAction(©FiltersAction);
3416 popup.addAction(&pasteFiltersAction);
3417 popup.addSeparator();
3418 popup.addAction(&toggleControlLayoutAction);
3419 popup.addSeparator();
3420 popup.addAction(&filtersAction);
3421 popup.addAction(&propertiesAction);
3422 popup.addAction(&advPropAction);
3423 popup.exec(QCursor::pos());
3424 vol->SetContextMenu(nullptr);
3425 }
3426
3427 void OBSBasic::on_hMixerScrollArea_customContextMenuRequested()
3428 {
3429 StackedMixerAreaContextMenuRequested();
3430 }
3431
3432 void OBSBasic::on_vMixerScrollArea_customContextMenuRequested()
3433 {
3434 StackedMixerAreaContextMenuRequested();
3435 }
3436
3437 void OBSBasic::StackedMixerAreaContextMenuRequested()
3438 {
3439 QAction unhideAllAction(QTStr("UnhideAll"), this);
3440
3441 QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this);
3442
3443 QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this);
3444 toggleControlLayoutAction.setCheckable(true);
3445 toggleControlLayoutAction.setChecked(config_get_bool(
3446 GetGlobalConfig(), "BasicWindow", "VerticalVolControl"));
3447
3448 /* ------------------- */
3449
3450 connect(&unhideAllAction, &QAction::triggered, this,
3451 &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection);
3452
3453 connect(&advPropAction, &QAction::triggered, this,
3454 &OBSBasic::on_actionAdvAudioProperties_triggered,
3455 Qt::DirectConnection);
3456
3457 /* ------------------- */
3458
3459 connect(&toggleControlLayoutAction, &QAction::changed, this,
3460 &OBSBasic::ToggleVolControlLayout, Qt::DirectConnection);
3461
3462 /* ------------------- */
3463
3464 QMenu popup;
3465 popup.addAction(&unhideAllAction);
3466 popup.addSeparator();
3467 popup.addAction(&toggleControlLayoutAction);
3468 popup.addSeparator();
3469 popup.addAction(&advPropAction);
3470 popup.exec(QCursor::pos());
3471 }
3472
3473 void OBSBasic::ToggleMixerLayout(bool vertical)
3474 {
3475 if (vertical) {
3476 ui->stackedMixerArea->setMinimumSize(180, 220);
3477 ui->stackedMixerArea->setCurrentIndex(1);
3478 } else {
3479 ui->stackedMixerArea->setMinimumSize(220, 0);
3480 ui->stackedMixerArea->setCurrentIndex(0);
3481 }
3482 }
3483
3484 void OBSBasic::ToggleVolControlLayout()
3485 {
3486 bool vertical = !config_get_bool(GetGlobalConfig(), "BasicWindow",
3487 "VerticalVolControl");
3488 config_set_bool(GetGlobalConfig(), "BasicWindow", "VerticalVolControl",
3489 vertical);
3490 ToggleMixerLayout(vertical);
3491
3492 // We need to store it so we can delete current and then add
3493 // at the right order
3494 vector<OBSSource> sources;
3495 for (size_t i = 0; i != volumes.size(); i++)
3496 sources.emplace_back(volumes[i]->GetSource());
3497
3498 ClearVolumeControls();
3499
3500 for (const auto &source : sources)
3501 ActivateAudioSource(source);
3502 }
3503
3504 void OBSBasic::ActivateAudioSource(OBSSource source)
3505 {
3506 if (SourceMixerHidden(source))
3507 return;
3508 if (!obs_source_audio_active(source))
3509 return;
3510
3511 bool vertical = config_get_bool(GetGlobalConfig(), "BasicWindow",
3512 "VerticalVolControl");
3513 VolControl *vol = new VolControl(source, true, vertical);
3514
3515 vol->EnableSlider(!SourceVolumeLocked(source));
3516
3517 double meterDecayRate =
3518 config_get_double(basicConfig, "Audio", "MeterDecayRate");
3519 vol->SetMeterDecayRate(meterDecayRate);
3520
3521 uint32_t peakMeterTypeIdx =
3522 config_get_uint(basicConfig, "Audio", "PeakMeterType");
3523
3524 enum obs_peak_meter_type peakMeterType;
3525 switch (peakMeterTypeIdx) {
3526 case 0:
3527 peakMeterType = SAMPLE_PEAK_METER;
3528 break;
3529 case 1:
3530 peakMeterType = TRUE_PEAK_METER;
3531 break;
3532 default:
3533 peakMeterType = SAMPLE_PEAK_METER;
3534 break;
3535 }
3536
3537 vol->setPeakMeterType(peakMeterType);
3538
3539 vol->setContextMenuPolicy(Qt::CustomContextMenu);
3540
3541 connect(vol, &QWidget::customContextMenuRequested, this,
3542 &OBSBasic::VolControlContextMenu);
3543 connect(vol, &VolControl::ConfigClicked, this,
3544 &OBSBasic::VolControlContextMenu);
3545
3546 InsertQObjectByName(volumes, vol);
3547
3548 for (auto volume : volumes) {
3549 if (vertical)
3550 ui->vVolControlLayout->addWidget(volume);
3551 else
3552 ui->hVolControlLayout->addWidget(volume);
3553 }
3554 }
3555
3556 void OBSBasic::DeactivateAudioSource(OBSSource source)
3557 {
3558 for (size_t i = 0; i < volumes.size(); i++) {
3559 if (volumes[i]->GetSource() == source) {
3560 delete volumes[i];
3561 volumes.erase(volumes.begin() + i);
3562 break;
3563 }
3564 }
3565 }
3566
3567 bool OBSBasic::QueryRemoveSource(obs_source_t *source)
3568 {
3569 if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE &&
3570 !obs_source_is_group(source)) {
3571 int count = ui->scenes->count();
3572
3573 if (count == 1) {
3574 OBSMessageBox::information(this,
3575 QTStr("FinalScene.Title"),
3576 QTStr("FinalScene.Text"));
3577 return false;
3578 }
3579 }
3580
3581 const char *name = obs_source_get_name(source);
3582
3583 QString text = QTStr("ConfirmRemove.Text");
3584 text.replace("$1", QT_UTF8(name));
3585
3586 QMessageBox remove_source(this);
3587 remove_source.setText(text);
3588 QPushButton *Yes =
3589 remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole);
3590 remove_source.setDefaultButton(Yes);
3591 remove_source.addButton(QTStr("No"), QMessageBox::NoRole);
3592 remove_source.setIcon(QMessageBox::Question);
3593 remove_source.setWindowTitle(QTStr("ConfirmRemove.Title"));
3594 remove_source.exec();
3595
3596 return Yes == remove_source.clickedButton();
3597 }
3598
3599 #define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */
3600
3601 #ifdef UPDATE_SPARKLE
3602 void init_sparkle_updater(bool update_to_undeployed);
3603 void trigger_sparkle_update();
3604 #endif
3605
3606 void OBSBasic::TimedCheckForUpdates()
3607 {
3608 if (App()->IsUpdaterDisabled())
3609 return;
3610 if (!config_get_bool(App()->GlobalConfig(), "General",
3611 "EnableAutoUpdates"))
3612 return;
3613
3614 #ifdef UPDATE_SPARKLE
3615 init_sparkle_updater(config_get_bool(App()->GlobalConfig(), "General",
3616 "UpdateToUndeployed"));
3617 #elif _WIN32
3618 long long lastUpdate = config_get_int(App()->GlobalConfig(), "General",
3619 "LastUpdateCheck");
3620 uint32_t lastVersion =
3621 config_get_int(App()->GlobalConfig(), "General", "LastVersion");
3622
3623 if (lastVersion < LIBOBS_API_VER) {
3624 lastUpdate = 0;
3625 config_set_int(App()->GlobalConfig(), "General",
3626 "LastUpdateCheck", 0);
3627 }
3628
3629 long long t = (long long)time(nullptr);
3630 long long secs = t - lastUpdate;
3631
3632 if (secs > UPDATE_CHECK_INTERVAL)
3633 CheckForUpdates(false);
3634 #endif
3635 }
3636
3637 void OBSBasic::CheckForUpdates(bool manualUpdate)
3638 {
3639 #ifdef __FreeBSD__
3640 // Update check seg faults on FreeBSD
3641 return;
3642 #endif
3643 #ifdef UPDATE_SPARKLE
3644 trigger_sparkle_update();
3645 #elif _WIN32
3646 ui->actionCheckForUpdates->setEnabled(false);
3647
3648 if (updateCheckThread && updateCheckThread->isRunning())
3649 return;
3650
3651 updateCheckThread.reset(new AutoUpdateThread(manualUpdate));
3652 updateCheckThread->start();
3653 #endif
3654
3655 UNUSED_PARAMETER(manualUpdate);
3656 }
3657
3658 void OBSBasic::updateCheckFinished()
3659 {
3660 ui->actionCheckForUpdates->setEnabled(true);
3661 }
3662
3663 void OBSBasic::DuplicateSelectedScene()
3664 {
3665 OBSScene curScene = GetCurrentScene();
3666
3667 if (!curScene)
3668 return;
3669
3670 OBSSource curSceneSource = obs_scene_get_source(curScene);
3671 QString format{obs_source_get_name(curSceneSource)};
3672 format += " %1";
3673
3674 int i = 2;
3675 QString placeHolderText = format.arg(i);
3676 obs_source_t *source = nullptr;
3677 while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) {
3678 obs_source_release(source);
3679 placeHolderText = format.arg(++i);
3680 }
3681
3682 for (;;) {
3683 string name;
3684 bool accepted = NameDialog::AskForName(
3685 this, QTStr("Basic.Main.AddSceneDlg.Title"),
3686 QTStr("Basic.Main.AddSceneDlg.Text"), name,
3687 placeHolderText);
3688 if (!accepted)
3689 return;
3690
3691 if (name.empty()) {
3692 OBSMessageBox::warning(this,
3693 QTStr("NoNameEntered.Title"),
3694 QTStr("NoNameEntered.Text"));
3695 continue;
3696 }
3697
3698 obs_source_t *source = obs_get_source_by_name(name.c_str());
3699 if (source) {
3700 OBSMessageBox::warning(this, QTStr("NameExists.Title"),
3701 QTStr("NameExists.Text"));
3702
3703 obs_source_release(source);
3704 continue;
3705 }
3706
3707 obs_scene_t *scene = obs_scene_duplicate(curScene, name.c_str(),
3708 OBS_SCENE_DUP_REFS);
3709 source = obs_scene_get_source(scene);
3710 SetCurrentScene(source, true);
3711
3712 auto undo = [](const std::string &data) {
3713 obs_source_t *source =
3714 obs_get_source_by_name(data.c_str());
3715 obs_source_remove(source);
3716 obs_source_release(source);
3717 };
3718
3719 auto redo = [this, name](const std::string &data) {
3720 obs_source_t *source =
3721 obs_get_source_by_name(data.c_str());
3722 obs_scene_t *scene = obs_scene_from_source(source);
3723 obs_source_release(source);
3724 scene = obs_scene_duplicate(scene, name.c_str(),
3725 OBS_SCENE_DUP_REFS);
3726 source = obs_scene_get_source(scene);
3727 SetCurrentScene(source, true);
3728 obs_scene_release(scene);
3729 };
3730
3731 undo_s.add_action(
3732 QTStr("Undo.Scene.Duplicate")
3733 .arg(obs_source_get_name(source)),
3734 undo, redo, obs_source_get_name(source),
3735 obs_source_get_name(obs_scene_get_source(curScene)));
3736
3737 obs_scene_release(scene);
3738
3739 break;
3740 }
3741 }
3742
3743 static bool save_undo_source_enum(obs_scene_t *scene, obs_sceneitem_t *item,
3744 void *p)
3745 {
3746 UNUSED_PARAMETER(scene);
3747
3748 obs_source_t *source = obs_sceneitem_get_source(item);
3749 if (obs_obj_is_private(source) && !obs_source_removed(source))
3750 return true;
3751
3752 obs_data_array_t *array = (obs_data_array_t *)p;
3753
3754 /* check if the source is already stored in the array */
3755 const char *name = obs_source_get_name(source);
3756 const size_t count = obs_data_array_count(array);
3757 for (size_t i = 0; i < count; i++) {
3758 obs_data_t *sourceData = obs_data_array_item(array, i);
3759 if (strcmp(name, obs_data_get_string(sourceData, "name")) ==
3760 0) {
3761 obs_data_release(sourceData);
3762 return true;
3763 }
3764 obs_data_release(sourceData);
3765 }
3766
3767 if (obs_source_is_group(source))
3768 obs_scene_enum_items(obs_group_from_source(source),
3769 save_undo_source_enum, p);
3770
3771 obs_data_t *source_data = obs_save_source(source);
3772 obs_data_array_push_back(array, source_data);
3773 obs_data_release(source_data);
3774 return true;
3775 }
3776
3777 void OBSBasic::RemoveSelectedScene()
3778 {
3779 OBSScene scene = GetCurrentScene();
3780 obs_source_t *source = obs_scene_get_source(scene);
3781
3782 OBSSource curProgramScene = OBSGetStrongRef(programScene);
3783
3784 if (!source || !QueryRemoveSource(source)) {
3785 return;
3786 }
3787
3788 /* ------------------------------ */
3789 /* save all sources in scene */
3790
3791 obs_data_array_t *array = obs_data_array_create();
3792
3793 obs_scene_enum_items(scene, save_undo_source_enum, array);
3794
3795 obs_data_t *scene_data = obs_save_source(source);
3796 obs_data_array_push_back(array, scene_data);
3797 obs_data_release(scene_data);
3798
3799 /* ----------------------------------------------- */
3800 /* save all scenes and groups the scene is used in */
3801
3802 for (int i = 0; i < ui->scenes->count(); i++) {
3803 QListWidgetItem *widget_item = ui->scenes->item(i);
3804 auto item_scene = GetOBSRef<OBSScene>(widget_item);
3805 if (scene == item_scene)
3806 continue;
3807 auto *item = obs_scene_find_source_recursive(
3808 item_scene, obs_source_get_name(source));
3809 if (item) {
3810 scene_data = obs_save_source(obs_scene_get_source(
3811 obs_sceneitem_get_scene(item)));
3812 obs_data_array_push_back(array, scene_data);
3813 obs_data_release(scene_data);
3814 }
3815 }
3816
3817 /* --------------------------- */
3818 /* undo/redo */
3819
3820 auto undo = [this](const std::string &json) {
3821 obs_data_t *base = obs_data_create_from_json(json.c_str());
3822 obs_data_array_t *array = obs_data_get_array(base, "array");
3823 int savedIndex = (int)obs_data_get_int(base, "index");
3824 std::vector<obs_source_t *> sources;
3825
3826 /* create missing sources */
3827 size_t count = obs_data_array_count(array);
3828 sources.reserve(count);
3829
3830 for (size_t i = 0; i < count; i++) {
3831 obs_data_t *data = obs_data_array_item(array, i);
3832 const char *name = obs_data_get_string(data, "name");
3833
3834 obs_source_t *source = obs_get_source_by_name(name);
3835 if (!source)
3836 source = obs_load_source(data);
3837 sources.push_back(source);
3838
3839 obs_data_release(data);
3840 }
3841
3842 /* actually load sources now */
3843 for (obs_source_t *source : sources)
3844 obs_source_load2(source);
3845
3846 obs_source_t *scene_source = sources.back();
3847 OBSScene scene = obs_scene_from_source(scene_source);
3848 SetCurrentScene(scene, true);
3849
3850 /* set original index in list box */
3851 ui->scenes->blockSignals(true);
3852 int curIndex = ui->scenes->currentRow();
3853 QListWidgetItem *item = ui->scenes->takeItem(curIndex);
3854 ui->scenes->insertItem(savedIndex, item);
3855 ui->scenes->setCurrentRow(savedIndex);
3856 ui->scenes->blockSignals(false);
3857
3858 /* release sources */
3859 for (obs_source_t *source : sources)
3860 obs_source_release(source);
3861
3862 obs_data_array_release(array);
3863 obs_data_release(base);
3864 };
3865
3866 auto redo = [](const std::string &name) {
3867 obs_source_t *source = obs_get_source_by_name(name.c_str());
3868 obs_source_remove(source);
3869 obs_source_release(source);
3870 };
3871
3872 obs_data_t *data = obs_data_create();
3873 obs_data_set_array(data, "array", array);
3874 obs_data_set_int(data, "index", ui->scenes->currentRow());
3875
3876 const char *scene_name = obs_source_get_name(source);
3877 undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo,
3878 obs_data_get_json(data), scene_name);
3879
3880 obs_data_array_release(array);
3881 obs_data_release(data);
3882
3883 /* --------------------------- */
3884 /* remove */
3885
3886 obs_source_remove(source);
3887
3888 if (api)
3889 api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
3890 }
3891
3892 void OBSBasic::ReorderSources(OBSScene scene)
3893 {
3894 if (scene != GetCurrentScene() || ui->sources->IgnoreReorder())
3895 return;
3896
3897 ui->sources->ReorderItems();
3898 SaveProject();
3899 }
3900
3901 void OBSBasic::RefreshSources(OBSScene scene)
3902 {
3903 if (scene != GetCurrentScene() || ui->sources->IgnoreReorder())
3904 return;
3905
3906 ui->sources->RefreshItems();
3907 SaveProject();
3908 }
3909
3910 /* OBS Callbacks */
3911
3912 void OBSBasic::SceneReordered(void *data, calldata_t *params)
3913 {
3914 OBSBasic *window = static_cast<OBSBasic *>(data);
3915
3916 obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene");
3917
3918 QMetaObject::invokeMethod(window, "ReorderSources",
3919 Q_ARG(OBSScene, OBSScene(scene)));
3920 }
3921
3922 void OBSBasic::SceneRefreshed(void *data, calldata_t *params)
3923 {
3924 OBSBasic *window = static_cast<OBSBasic *>(data);
3925
3926 obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene");
3927
3928 QMetaObject::invokeMethod(window, "RefreshSources",
3929 Q_ARG(OBSScene, OBSScene(scene)));
3930 }
3931
3932 void OBSBasic::SceneItemAdded(void *data, calldata_t *params)
3933 {
3934 OBSBasic *window = static_cast<OBSBasic *>(data);
3935
3936 obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item");
3937
3938 QMetaObject::invokeMethod(window, "AddSceneItem",
3939 Q_ARG(OBSSceneItem, OBSSceneItem(item)));
3940 }
3941
3942 void OBSBasic::SourceCreated(void *data, calldata_t *params)
3943 {
3944 obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3945
3946 if (obs_scene_from_source(source) != NULL)
3947 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3948 "AddScene", WaitConnection(),
3949 Q_ARG(OBSSource, OBSSource(source)));
3950 }
3951
3952 void OBSBasic::SourceRemoved(void *data, calldata_t *params)
3953 {
3954 obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3955
3956 if (obs_scene_from_source(source) != NULL)
3957 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3958 "RemoveScene",
3959 Q_ARG(OBSSource, OBSSource(source)));
3960 }
3961
3962 void OBSBasic::SourceActivated(void *data, calldata_t *params)
3963 {
3964 obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3965 uint32_t flags = obs_source_get_output_flags(source);
3966
3967 if (flags & OBS_SOURCE_AUDIO)
3968 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3969 "ActivateAudioSource",
3970 Q_ARG(OBSSource, OBSSource(source)));
3971 }
3972
3973 void OBSBasic::SourceDeactivated(void *data, calldata_t *params)
3974 {
3975 obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3976 uint32_t flags = obs_source_get_output_flags(source);
3977
3978 if (flags & OBS_SOURCE_AUDIO)
3979 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3980 "DeactivateAudioSource",
3981 Q_ARG(OBSSource, OBSSource(source)));
3982 }
3983
3984 void OBSBasic::SourceAudioActivated(void *data, calldata_t *params)
3985 {
3986 obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3987
3988 if (obs_source_active(source))
3989 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3990 "ActivateAudioSource",
3991 Q_ARG(OBSSource, OBSSource(source)));
3992 }
3993
3994 void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params)
3995 {
3996 obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3997 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3998 "DeactivateAudioSource",
3999 Q_ARG(OBSSource, OBSSource(source)));
4000 }
4001
4002 void OBSBasic::SourceRenamed(void *data, calldata_t *params)
4003 {
4004 obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
4005 const char *newName = calldata_string(params, "new_name");
4006 const char *prevName = calldata_string(params, "prev_name");
4007
4008 QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
4009 "RenameSources", Q_ARG(OBSSource, source),
4010 Q_ARG(QString, QT_UTF8(newName)),
4011 Q_ARG(QString, QT_UTF8(prevName)));
4012
4013 blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName);
4014 }
4015
4016 void OBSBasic::DrawBackdrop(float cx, float cy)
4017 {
4018 if (!box)
4019 return;
4020
4021 GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop");
4022
4023 gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
4024 gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color");
4025 gs_technique_t *tech = gs_effect_get_technique(solid, "Solid");
4026
4027 vec4 colorVal;
4028 vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f);
4029 gs_effect_set_vec4(color, &colorVal);
4030
4031 gs_technique_begin(tech);
4032 gs_technique_begin_pass(tech, 0);
4033 gs_matrix_push();
4034 gs_matrix_identity();
4035 gs_matrix_scale3f(float(cx), float(cy), 1.0f);
4036
4037 gs_load_vertexbuffer(box);
4038 gs_draw(GS_TRISTRIP, 0, 0);
4039
4040 gs_matrix_pop();
4041 gs_technique_end_pass(tech);
4042 gs_technique_end(tech);
4043
4044 gs_load_vertexbuffer(nullptr);
4045
4046 GS_DEBUG_MARKER_END();
4047 }
4048
4049 void OBSBasic::RenderMain(void *data, uint32_t cx, uint32_t cy)
4050 {
4051 GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain");
4052
4053 OBSBasic *window = static_cast<OBSBasic *>(data);
4054 obs_video_info ovi;
4055
4056 obs_get_video_info(&ovi);
4057
4058 window->previewCX = int(window->previewScale * float(ovi.base_width));
4059 window->previewCY = int(window->previewScale * float(ovi.base_height));
4060
4061 gs_viewport_push();
4062 gs_projection_push();
4063
4064 obs_display_t *display = window->ui->preview->GetDisplay();
4065 uint32_t width, height;
4066 obs_display_size(display, &width, &height);
4067 float right = float(width) - window->previewX;
4068 float bottom = float(height) - window->previewY;
4069
4070 gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f,
4071 100.0f);
4072
4073 window->ui->preview->DrawOverflow();
4074
4075 /* --------------------------------------- */
4076
4077 gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height),
4078 -100.0f, 100.0f);
4079 gs_set_viewport(window->previewX, window->previewY, window->previewCX,
4080 window->previewCY);
4081
4082 if (window->IsPreviewProgramMode()) {
4083 window->DrawBackdrop(float(ovi.base_width),
4084 float(ovi.base_height));
4085
4086 OBSScene scene = window->GetCurrentScene();
4087 obs_source_t *source = obs_scene_get_source(scene);
4088 if (source)
4089 obs_source_video_render(source);
4090 } else {
4091 obs_render_main_texture_src_color_only();
4092 }
4093 gs_load_vertexbuffer(nullptr);
4094
4095 /* --------------------------------------- */
4096
4097 gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f,
4098 100.0f);
4099 gs_reset_viewport();
4100
4101 window->ui->preview->DrawSceneEditing();
4102
4103 uint32_t targetCX = window->previewCX;
4104 uint32_t targetCY = window->previewCY;
4105
4106 if (window->drawSafeAreas) {
4107 RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY);
4108 RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY);
4109 RenderSafeAreas(window->fourByThreeSafeMargin, targetCX,
4110 targetCY);
4111 RenderSafeAreas(window->leftLine, targetCX, targetCY);
4112 RenderSafeAreas(window->topLine, targetCX, targetCY);
4113 RenderSafeAreas(window->rightLine, targetCX, targetCY);
4114 }
4115
4116 /* --------------------------------------- */
4117
4118 gs_projection_pop();
4119 gs_viewport_pop();
4120
4121 GS_DEBUG_MARKER_END();
4122
4123 UNUSED_PARAMETER(cx);
4124 UNUSED_PARAMETER(cy);
4125 }
4126
4127 /* Main class functions */
4128
4129 obs_service_t *OBSBasic::GetService()
4130 {
4131 if (!service) {
4132 service =
4133 obs_service_create("rtmp_common", NULL, NULL, nullptr);
4134 obs_service_release(service);
4135 }
4136 return service;
4137 }
4138
4139 void OBSBasic::SetService(obs_service_t *newService)
4140 {
4141 if (newService)
4142 service = newService;
4143 }
4144
4145 int OBSBasic::GetTransitionDuration()
4146 {
4147 return ui->transitionDuration->value();
4148 }
4149
4150 bool OBSBasic::StreamingActive() const
4151 {
4152 if (!outputHandler)
4153 return false;
4154 return outputHandler->StreamingActive();
4155 }
4156
4157 bool OBSBasic::Active() const
4158 {
4159 if (!outputHandler)
4160 return false;
4161 return outputHandler->Active();
4162 }
4163
4164 #ifdef _WIN32
4165 #define IS_WIN32 1
4166 #else
4167 #define IS_WIN32 0
4168 #endif
4169
4170 static inline int AttemptToResetVideo(struct obs_video_info *ovi)
4171 {
4172 return obs_reset_video(ovi);
4173 }
4174
4175 static inline enum obs_scale_type GetScaleType(ConfigFile &basicConfig)
4176 {
4177 const char *scaleTypeStr =
4178 config_get_string(basicConfig, "Video", "ScaleType");
4179
4180 if (astrcmpi(scaleTypeStr, "bilinear") == 0)
4181 return OBS_SCALE_BILINEAR;
4182 else if (astrcmpi(scaleTypeStr, "lanczos") == 0)
4183 return OBS_SCALE_LANCZOS;
4184 else if (astrcmpi(scaleTypeStr, "area") == 0)
4185 return OBS_SCALE_AREA;
4186 else
4187 return OBS_SCALE_BICUBIC;
4188 }
4189
4190 static inline enum video_format GetVideoFormatFromName(const char *name)
4191 {
4192 if (astrcmpi(name, "I420") == 0)
4193 return VIDEO_FORMAT_I420;
4194 else if (astrcmpi(name, "NV12") == 0)
4195 return VIDEO_FORMAT_NV12;
4196 else if (astrcmpi(name, "I444") == 0)
4197 return VIDEO_FORMAT_I444;
4198 #if 0 //currently unsupported
4199 else if (astrcmpi(name, "YVYU") == 0)
4200 return VIDEO_FORMAT_YVYU;
4201 else if (astrcmpi(name, "YUY2") == 0)
4202 return VIDEO_FORMAT_YUY2;
4203 else if (astrcmpi(name, "UYVY") == 0)
4204 return VIDEO_FORMAT_UYVY;
4205 #endif
4206 else
4207 return VIDEO_FORMAT_RGBA;
4208 }
4209
4210 void OBSBasic::ResetUI()
4211 {
4212 bool studioPortraitLayout = config_get_bool(
4213 GetGlobalConfig(), "BasicWindow", "StudioPortraitLayout");
4214
4215 bool labels = config_get_bool(GetGlobalConfig(), "BasicWindow",
4216 "StudioModeLabels");
4217
4218 if (studioPortraitLayout)
4219 ui->previewLayout->setDirection(QBoxLayout::TopToBottom);
4220 else
4221 ui->previewLayout->setDirection(QBoxLayout::LeftToRight);
4222
4223 if (previewProgramMode)
4224 ui->previewLabel->setHidden(!labels);
4225
4226 if (programLabel)
4227 programLabel->setHidden(!labels);
4228 }
4229
4230 int OBSBasic::ResetVideo()
4231 {
4232 if (outputHandler && outputHandler->Active())
4233 return OBS_VIDEO_CURRENTLY_ACTIVE;
4234
4235 ProfileScope("OBSBasic::ResetVideo");
4236
4237 struct obs_video_info ovi;
4238 int ret;
4239
4240 GetConfigFPS(ovi.fps_num, ovi.fps_den);
4241
4242 const char *colorFormat =
4243 config_get_string(basicConfig, "Video", "ColorFormat");
4244 const char *colorSpace =
4245 config_get_string(basicConfig, "Video", "ColorSpace");
4246 const char *colorRange =
4247 config_get_string(basicConfig, "Video", "ColorRange");
4248
4249 ovi.graphics_module = App()->GetRenderModule();
4250 ovi.base_width =
4251 (uint32_t)config_get_uint(basicConfig, "Video", "BaseCX");
4252 ovi.base_height =
4253 (uint32_t)config_get_uint(basicConfig, "Video", "BaseCY");
4254 ovi.output_width =
4255 (uint32_t)config_get_uint(basicConfig, "Video", "OutputCX");
4256 ovi.output_height =
4257 (uint32_t)config_get_uint(basicConfig, "Video", "OutputCY");
4258 ovi.output_format = GetVideoFormatFromName(colorFormat);
4259 ovi.colorspace = astrcmpi(colorSpace, "601") == 0
4260 ? VIDEO_CS_601
4261 : (astrcmpi(colorSpace, "709") == 0
4262 ? VIDEO_CS_709
4263 : VIDEO_CS_SRGB);
4264 ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL
4265 : VIDEO_RANGE_PARTIAL;
4266 ovi.adapter =
4267 config_get_uint(App()->GlobalConfig(), "Video", "AdapterIdx");
4268 ovi.gpu_conversion = true;
4269 ovi.scale_type = GetScaleType(basicConfig);
4270
4271 if (ovi.base_width < 8 || ovi.base_height < 8) {
4272 ovi.base_width = 1920;
4273 ovi.base_height = 1080;
4274 config_set_uint(basicConfig, "Video", "BaseCX", 1920);
4275 config_set_uint(basicConfig, "Video", "BaseCY", 1080);
4276 }
4277
4278 if (ovi.output_width < 8 || ovi.output_height < 8) {
4279 ovi.output_width = ovi.base_width;
4280 ovi.output_height = ovi.base_height;
4281 config_set_uint(basicConfig, "Video", "OutputCX",
4282 ovi.base_width);
4283 config_set_uint(basicConfig, "Video", "OutputCY",
4284 ovi.base_height);
4285 }
4286
4287 ret = AttemptToResetVideo(&ovi);
4288 if (IS_WIN32 && ret != OBS_VIDEO_SUCCESS) {
4289 if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) {
4290 blog(LOG_WARNING, "Tried to reset when "
4291 "already active");
4292 return ret;
4293 }
4294
4295 /* Try OpenGL if DirectX fails on windows */
4296 if (astrcmpi(ovi.graphics_module, DL_OPENGL) != 0) {
4297 blog(LOG_WARNING,
4298 "Failed to initialize obs video (%d) "
4299 "with graphics_module='%s', retrying "
4300 "with graphics_module='%s'",
4301 ret, ovi.graphics_module, DL_OPENGL);
4302 ovi.graphics_module = DL_OPENGL;
4303 ret = AttemptToResetVideo(&ovi);
4304 }
4305 } else if (ret == OBS_VIDEO_SUCCESS) {
4306 ResizePreview(ovi.base_width, ovi.base_height);
4307 if (program)
4308 ResizeProgram(ovi.base_width, ovi.base_height);
4309 }
4310
4311 if (ret == OBS_VIDEO_SUCCESS) {
4312 OBSBasicStats::InitializeValues();
4313 OBSProjector::UpdateMultiviewProjectors();
4314 }
4315
4316 return ret;
4317 }
4318
4319 bool OBSBasic::ResetAudio()
4320 {
4321 ProfileScope("OBSBasic::ResetAudio");
4322
4323 struct obs_audio_info ai;
4324 ai.samples_per_sec =
4325 config_get_uint(basicConfig, "Audio", "SampleRate");
4326
4327 const char *channelSetupStr =
4328 config_get_string(basicConfig, "Audio", "ChannelSetup");
4329
4330 if (strcmp(channelSetupStr, "Mono") == 0)
4331 ai.speakers = SPEAKERS_MONO;
4332 else if (strcmp(channelSetupStr, "2.1") == 0)
4333 ai.speakers = SPEAKERS_2POINT1;
4334 else if (strcmp(channelSetupStr, "4.0") == 0)
4335 ai.speakers = SPEAKERS_4POINT0;
4336 else if (strcmp(channelSetupStr, "4.1") == 0)
4337 ai.speakers = SPEAKERS_4POINT1;
4338 else if (strcmp(channelSetupStr, "5.1") == 0)
4339 ai.speakers = SPEAKERS_5POINT1;
4340 else if (strcmp(channelSetupStr, "7.1") == 0)
4341 ai.speakers = SPEAKERS_7POINT1;
4342 else
4343 ai.speakers = SPEAKERS_STEREO;
4344
4345 return obs_reset_audio(&ai);
4346 }
4347
4348 void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId,
4349 const char *deviceDesc, int channel)
4350 {
4351 bool disable = deviceId && strcmp(deviceId, "disabled") == 0;
4352 obs_source_t *source;
4353 obs_data_t *settings;
4354
4355 source = obs_get_output_source(channel);
4356 if (source) {
4357 if (disable) {
4358 obs_set_output_source(channel, nullptr);
4359 } else {
4360 settings = obs_source_get_settings(source);
4361 const char *oldId =
4362 obs_data_get_string(settings, "device_id");
4363 if (strcmp(oldId, deviceId) != 0) {
4364 obs_data_set_string(settings, "device_id",
4365 deviceId);
4366 obs_source_update(source, settings);
4367 }
4368 obs_data_release(settings);
4369 }
4370
4371 obs_source_release(source);
4372
4373 } else if (!disable) {
4374 settings = obs_data_create();
4375 obs_data_set_string(settings, "device_id", deviceId);
4376 source = obs_source_create(sourceId, deviceDesc, settings,
4377 nullptr);
4378 obs_data_release(settings);
4379
4380 obs_set_output_source(channel, source);
4381 obs_source_release(source);
4382 }
4383 }
4384
4385 void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy)
4386 {
4387 QSize targetSize;
4388 bool isFixedScaling;
4389 obs_video_info ovi;
4390
4391 /* resize preview panel to fix to the top section of the window */
4392 targetSize = GetPixelSize(ui->preview);
4393
4394 isFixedScaling = ui->preview->IsFixedScaling();
4395 obs_get_video_info(&ovi);
4396
4397 if (isFixedScaling) {
4398 previewScale = ui->preview->GetScalingAmount();
4399 GetCenterPosFromFixedScale(
4400 int(cx), int(cy),
4401 targetSize.width() - PREVIEW_EDGE_SIZE * 2,
4402 targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX,
4403 previewY, previewScale);
4404 previewX += ui->preview->GetScrollX();
4405 previewY += ui->preview->GetScrollY();
4406
4407 } else {
4408 GetScaleAndCenterPos(int(cx), int(cy),
4409 targetSize.width() - PREVIEW_EDGE_SIZE * 2,
4410 targetSize.height() -
4411 PREVIEW_EDGE_SIZE * 2,
4412 previewX, previewY, previewScale);
4413 }
4414
4415 previewX += float(PREVIEW_EDGE_SIZE);
4416 previewY += float(PREVIEW_EDGE_SIZE);
4417 }
4418
4419 void OBSBasic::CloseDialogs()
4420 {
4421 QList<QDialog *> childDialogs = this->findChildren<QDialog *>();
4422 if (!childDialogs.isEmpty()) {
4423 for (int i = 0; i < childDialogs.size(); ++i) {
4424 childDialogs.at(i)->close();
4425 }
4426 }
4427
4428 if (!stats.isNull())
4429 stats->close(); //call close to save Stats geometry
4430 if (!remux.isNull())
4431 remux->close();
4432 }
4433
4434 void OBSBasic::EnumDialogs()
4435 {
4436 visDialogs.clear();
4437 modalDialogs.clear();
4438 visMsgBoxes.clear();
4439
4440 /* fill list of Visible dialogs and Modal dialogs */
4441 QList<QDialog *> dialogs = findChildren<QDialog *>();
4442 for (QDialog *dialog : dialogs) {
4443 if (dialog->isVisible())
4444 visDialogs.append(dialog);
4445 if (dialog->isModal())
4446 modalDialogs.append(dialog);
4447 }
4448
4449 /* fill list of Visible message boxes */
4450 QList<QMessageBox *> msgBoxes = findChildren<QMessageBox *>();
4451 for (QMessageBox *msgbox : msgBoxes) {
4452 if (msgbox->isVisible())
4453 visMsgBoxes.append(msgbox);
4454 }
4455 }
4456
4457 void OBSBasic::ClearProjectors()
4458 {
4459 for (size_t i = 0; i < projectors.size(); i++) {
4460 if (projectors[i])
4461 delete projectors[i];
4462 }
4463
4464 projectors.clear();
4465 }
4466
4467 void OBSBasic::ClearSceneData()
4468 {
4469 disableSaving++;
4470
4471 CloseDialogs();
4472
4473 ClearVolumeControls();
4474 ClearListItems(ui->scenes);
4475 ui->sources->Clear();
4476 ClearQuickTransitions();
4477 ui->transitions->clear();
4478
4479 ClearProjectors();
4480
4481 for (int i = 0; i < MAX_CHANNELS; i++)
4482 obs_set_output_source(i, nullptr);
4483
4484 lastScene = nullptr;
4485 swapScene = nullptr;
4486 programScene = nullptr;
4487 prevFTBSource = nullptr;
4488
4489 copyStrings.clear();
4490 copyFiltersString = nullptr;
4491 copyFilter = nullptr;
4492
4493 auto cb = [](void *unused, obs_source_t *source) {
4494 obs_source_remove(source);
4495 UNUSED_PARAMETER(unused);
4496 return true;
4497 };
4498
4499 obs_enum_scenes(cb, nullptr);
4500 obs_enum_sources(cb, nullptr);
4501
4502 if (api)
4503 api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP);
4504
4505 undo_s.clear();
4506
4507 disableSaving--;
4508
4509 blog(LOG_INFO, "All scene data cleared");
4510 blog(LOG_INFO, "------------------------------------------------");
4511 }
4512
4513 void OBSBasic::closeEvent(QCloseEvent *event)
4514 {
4515 /* Do not close window if inside of a temporary event loop because we
4516 * could be inside of an Auth::LoadUI call. Keep trying once per
4517 * second until we've exit any known sub-loops. */
4518 if (os_atomic_load_long(&insideEventLoop) != 0) {
4519 QTimer::singleShot(1000, this, SLOT(close()));
4520 event->ignore();
4521 return;
4522 }
4523
4524 #if YOUTUBE_ENABLED
4525 /* Also don't close the window if the youtube stream check is active */
4526 if (youtubeStreamCheckThread) {
4527 QTimer::singleShot(1000, this, SLOT(close()));
4528 event->ignore();
4529 return;
4530 }
4531 #endif
4532
4533 if (isVisible())
4534 config_set_string(App()->GlobalConfig(), "BasicWindow",
4535 "geometry",
4536 saveGeometry().toBase64().constData());
4537
4538 if (outputHandler && outputHandler->Active()) {
4539 SetShowing(true);
4540
4541 QMessageBox::StandardButton button = OBSMessageBox::question(
4542 this, QTStr("ConfirmExit.Title"),
4543 QTStr("ConfirmExit.Text"));
4544
4545 if (button == QMessageBox::No) {
4546 event->ignore();
4547 restart = false;
4548 return;
4549 }
4550 }
4551
4552 QWidget::closeEvent(event);
4553 if (!event->isAccepted())
4554 return;
4555
4556 blog(LOG_INFO, SHUTDOWN_SEPARATOR);
4557
4558 closing = true;
4559
4560 if (introCheckThread)
4561 introCheckThread->wait();
4562 if (whatsNewInitThread)
4563 whatsNewInitThread->wait();
4564 if (updateCheckThread)
4565 updateCheckThread->wait();
4566 if (logUploadThread)
4567 logUploadThread->wait();
4568 if (devicePropertiesThread && devicePropertiesThread->isRunning()) {
4569 devicePropertiesThread->wait();
4570 devicePropertiesThread.reset();
4571 }
4572
4573 QApplication::sendPostedEvents(this);
4574
4575 signalHandlers.clear();
4576
4577 Auth::Save();
4578 SaveProjectNow();
4579 auth.reset();
4580
4581 delete extraBrowsers;
4582
4583 config_set_string(App()->GlobalConfig(), "BasicWindow", "DockState",
4584 saveState().toBase64().constData());
4585
4586 #ifdef BROWSER_AVAILABLE
4587 SaveExtraBrowserDocks();
4588 ClearExtraBrowserDocks();
4589 #endif
4590
4591 disableSaving++;
4592
4593 /* Clear all scene data (dialogs, widgets, widget sub-items, scenes,
4594 * sources, etc) so that all references are released before shutdown */
4595 ClearSceneData();
4596
4597 if (api)
4598 api->on_event(OBS_FRONTEND_EVENT_EXIT);
4599
4600 App()->quit();
4601 }
4602
4603 void OBSBasic::changeEvent(QEvent *event)
4604 {
4605 if (event->type() == QEvent::WindowStateChange) {
4606 QWindowStateChangeEvent *stateEvent =
4607 (QWindowStateChangeEvent *)event;
4608
4609 if (isMinimized()) {
4610 if (trayIcon && trayIcon->isVisible() &&
4611 sysTrayMinimizeToTray()) {
4612 ToggleShowHide();
4613 }
4614
4615 if (previewEnabled)
4616 EnablePreviewDisplay(false);
4617 } else if (stateEvent->oldState() & Qt::WindowMinimized &&
4618 isVisible()) {
4619 if (previewEnabled)
4620 EnablePreviewDisplay(true);
4621 }
4622 }
4623 }
4624
4625 void OBSBasic::on_actionShow_Recordings_triggered()
4626 {
4627 const char *mode = config_get_string(basicConfig, "Output", "Mode");
4628 const char *type = config_get_string(basicConfig, "AdvOut", "RecType");
4629 const char *adv_path =
4630 strcmp(type, "Standard")
4631 ? config_get_string(basicConfig, "AdvOut", "FFFilePath")
4632 : config_get_string(basicConfig, "AdvOut",
4633 "RecFilePath");
4634 const char *path = strcmp(mode, "Advanced")
4635 ? config_get_string(basicConfig,
4636 "SimpleOutput",
4637 "FilePath")
4638 : adv_path;
4639 QDesktopServices::openUrl(QUrl::fromLocalFile(path));
4640 }
4641
4642 void OBSBasic::on_actionRemux_triggered()
4643 {
4644 if (!remux.isNull()) {
4645 remux->show();
4646 remux->raise();
4647 return;
4648 }
4649
4650 const char *mode = config_get_string(basicConfig, "Output", "Mode");
4651 const char *path = strcmp(mode, "Advanced")
4652 ? config_get_string(basicConfig,
4653 "SimpleOutput",
4654 "FilePath")
4655 : config_get_string(basicConfig, "AdvOut",
4656 "RecFilePath");
4657
4658 OBSRemux *remuxDlg;
4659 remuxDlg = new OBSRemux(path, this);
4660 remuxDlg->show();
4661 remux = remuxDlg;
4662 }
4663
4664 void OBSBasic::on_action_Settings_triggered()
4665 {
4666 static bool settings_already_executing = false;
4667
4668 /* Do not load settings window if inside of a temporary event loop
4669 * because we could be inside of an Auth::LoadUI call. Keep trying
4670 * once per second until we've exit any known sub-loops. */
4671 if (os_atomic_load_long(&insideEventLoop) != 0) {
4672 QTimer::singleShot(1000, this,
4673 SLOT(on_action_Settings_triggered()));
4674 return;
4675 }
4676
4677 if (settings_already_executing) {
4678 return;
4679 }
4680
4681 settings_already_executing = true;
4682
4683 {
4684 OBSBasicSettings settings(this);
4685 settings.exec();
4686 }
4687
4688 SystemTray(false);
4689
4690 settings_already_executing = false;
4691
4692 if (restart) {
4693 QMessageBox::StandardButton button = OBSMessageBox::question(
4694 this, QTStr("Restart"), QTStr("NeedsRestart"));
4695
4696 if (button == QMessageBox::Yes)
4697 close();
4698 else
4699 restart = false;
4700 }
4701 }
4702
4703 static inline void AddMissingFiles(void *data, obs_source_t *source)
4704 {
4705 obs_missing_files_t *f = (obs_missing_files_t *)data;
4706 obs_missing_files_t *sf = obs_source_get_missing_files(source);
4707
4708 obs_missing_files_append(f, sf);
4709 obs_missing_files_destroy(sf);
4710 }
4711
4712 void OBSBasic::on_actionShowMissingFiles_triggered()
4713 {
4714 obs_missing_files_t *files = obs_missing_files_create();
4715
4716 auto cb_sources = [](void *data, obs_source_t *source) {
4717 AddMissingFiles(data, source);
4718 return true;
4719 };
4720 obs_enum_sources(cb_sources, files);
4721
4722 auto cb_transitions = [](void *data, obs_source_t *source) {
4723 if (obs_source_get_type(source) != OBS_SOURCE_TYPE_TRANSITION)
4724 return true;
4725
4726 AddMissingFiles(data, source);
4727 return true;
4728 };
4729 obs_enum_all_sources(cb_transitions, files);
4730
4731 if (obs_missing_files_count(files) > 0) {
4732 missDialog = new OBSMissingFiles(files, this);
4733 missDialog->setAttribute(Qt::WA_DeleteOnClose, true);
4734 missDialog->show();
4735 missDialog->raise();
4736 } else {
4737 obs_missing_files_destroy(files);
4738 OBSMessageBox::information(
4739 this, QTStr("MissingFiles.NoMissing.Title"),
4740 QTStr("MissingFiles.NoMissing.Text"));
4741 }
4742 }
4743
4744 void save_audio_source(int channel, obs_data_t *save)
4745 {
4746 obs_source_t *source = obs_get_output_source(channel);
4747 if (!source)
4748 return;
4749
4750 obs_data_t *obj = obs_data_create();
4751
4752 obs_data_set_double(obj, "vol", obs_source_get_volume(source));
4753 obs_data_set_double(obj, "balance",
4754 obs_source_get_balance_value(source));
4755 obs_data_set_double(obj, "mixers", obs_source_get_audio_mixers(source));
4756 obs_data_set_double(obj, "sync", obs_source_get_sync_offset(source));
4757 obs_data_set_double(obj, "flags", obs_source_get_flags(source));
4758
4759 obs_data_set_obj(save, std::to_string(channel).c_str(), obj);
4760 obs_data_release(obj);
4761 obs_source_release(source);
4762 }
4763
4764 void load_audio_source(int channel, obs_data_t *data)
4765 {
4766 obs_source_t *source = obs_get_output_source(channel);
4767 if (!source)
4768 return;
4769
4770 obs_data_t *save =
4771 obs_data_get_obj(data, std::to_string(channel).c_str());
4772
4773 obs_source_set_volume(source, obs_data_get_double(save, "vol"));
4774 obs_source_set_balance_value(source,
4775 obs_data_get_double(save, "balance"));
4776 obs_source_set_audio_mixers(source,
4777 obs_data_get_double(save, "mixers"));
4778 obs_source_set_sync_offset(source, obs_data_get_double(save, "sync"));
4779 obs_source_set_flags(source, obs_data_get_double(save, "flags"));
4780
4781 obs_data_release(save);
4782 obs_source_release(source);
4783 }
4784
4785 void OBSBasic::on_actionAdvAudioProperties_triggered()
4786 {
4787 if (advAudioWindow != nullptr) {
4788 advAudioWindow->raise();
4789 return;
4790 }
4791
4792 bool iconsVisible = config_get_bool(App()->GlobalConfig(),
4793 "BasicWindow", "ShowSourceIcons");
4794
4795 advAudioWindow = new OBSBasicAdvAudio(this);
4796 advAudioWindow->show();
4797 advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true);
4798 advAudioWindow->SetIconsVisible(iconsVisible);
4799
4800 connect(advAudioWindow, SIGNAL(destroyed()), this,
4801 SLOT(AdvAudioPropsDestroyed()));
4802 }
4803
4804 void OBSBasic::AdvAudioPropsClicked()
4805 {
4806 on_actionAdvAudioProperties_triggered();
4807 }
4808
4809 void OBSBasic::AdvAudioPropsDestroyed()
4810 {
4811 advAudioWindow = nullptr;
4812 }
4813
4814 void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current,
4815 QListWidgetItem *prev)
4816 {
4817 obs_source_t *source = NULL;
4818
4819 if (current) {
4820 obs_scene_t *scene;
4821
4822 scene = GetOBSRef<OBSScene>(current);
4823 source = obs_scene_get_source(scene);
4824 }
4825
4826 SetCurrentScene(source);
4827
4828 if (api)
4829 api->on_event(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED);
4830
4831 UpdateContextBar();
4832
4833 UNUSED_PARAMETER(prev);
4834 }
4835
4836 void OBSBasic::EditSceneName()
4837 {
4838 ui->scenesDock->removeAction(renameScene);
4839 QListWidgetItem *item = ui->scenes->currentItem();
4840 Qt::ItemFlags flags = item->flags();
4841
4842 item->setFlags(flags | Qt::ItemIsEditable);
4843 ui->scenes->editItem(item);
4844 item->setFlags(flags);
4845 }
4846
4847 void OBSBasic::AddProjectorMenuMonitors(QMenu *parent, QObject *target,
4848 const char *slot)
4849 {
4850 QAction *action;
4851 QList<QScreen *> screens = QGuiApplication::screens();
4852 for (int i = 0; i < screens.size(); i++) {
4853 QScreen *screen = screens[i];
4854 QRect screenGeometry = screen->geometry();
4855 qreal ratio = screen->devicePixelRatio();
4856 QString name = "";
4857 #ifdef _WIN32
4858 QTextStream fullname(&name);
4859 fullname << GetMonitorName(screen->name());
4860 fullname << " (";
4861 fullname << (i + 1);
4862 fullname << ")";
4863 #elif defined(__APPLE__)
4864 name = screen->name();
4865 #else
4866 name = screen->model().simplified();
4867
4868 if (name.length() > 1 && name.endsWith("-"))
4869 name.chop(1);
4870 #endif
4871 name = name.simplified();
4872
4873 if (name.length() == 0) {
4874 name = QString("%1 %2")
4875 .arg(QTStr("Display"))
4876 .arg(QString::number(i + 1));
4877 }
4878 QString str =
4879 QString("%1: %2x%3 @ %4,%5")
4880 .arg(name,
4881 QString::number(screenGeometry.width() *
4882 ratio),
4883 QString::number(screenGeometry.height() *
4884 ratio),
4885 QString::number(screenGeometry.x()),
4886 QString::number(screenGeometry.y()));
4887
4888 action = parent->addAction(str, target, slot);
4889 action->setProperty("monitor", i);
4890 }
4891 }
4892
4893 void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos)
4894 {
4895 QListWidgetItem *item = ui->scenes->itemAt(pos);
4896
4897 QMenu popup(this);
4898 QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this);
4899
4900 popup.addAction(QTStr("Add"), this,
4901 SLOT(on_actionAddScene_triggered()));
4902
4903 if (item) {
4904 QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this);
4905 copyFilters->setEnabled(false);
4906 connect(copyFilters, SIGNAL(triggered()), this,
4907 SLOT(SceneCopyFilters()));
4908 QAction *pasteFilters =
4909 new QAction(QTStr("Paste.Filters"), this);
4910 pasteFilters->setEnabled(copyFiltersString);
4911 connect(pasteFilters, SIGNAL(triggered()), this,
4912 SLOT(ScenePasteFilters()));
4913
4914 popup.addSeparator();
4915 popup.addAction(QTStr("Duplicate"), this,
4916 SLOT(DuplicateSelectedScene()));
4917 popup.addAction(copyFilters);
4918 popup.addAction(pasteFilters);
4919 popup.addSeparator();
4920 popup.addAction(QTStr("Rename"), this, SLOT(EditSceneName()));
4921 popup.addAction(QTStr("Remove"), this,
4922 SLOT(RemoveSelectedScene()));
4923 popup.addSeparator();
4924
4925 order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this,
4926 SLOT(on_actionSceneUp_triggered()));
4927 order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"),
4928 this, SLOT(on_actionSceneDown_triggered()));
4929 order.addSeparator();
4930 order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"),
4931 this, SLOT(MoveSceneToTop()));
4932 order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"),
4933 this, SLOT(MoveSceneToBottom()));
4934 popup.addMenu(&order);
4935
4936 popup.addSeparator();
4937
4938 delete sceneProjectorMenu;
4939 sceneProjectorMenu = new QMenu(QTStr("SceneProjector"));
4940 AddProjectorMenuMonitors(sceneProjectorMenu, this,
4941 SLOT(OpenSceneProjector()));
4942 popup.addMenu(sceneProjectorMenu);
4943
4944 QAction *sceneWindow = popup.addAction(
4945 QTStr("SceneWindow"), this, SLOT(OpenSceneWindow()));
4946
4947 popup.addAction(sceneWindow);
4948 popup.addAction(QTStr("Screenshot.Scene"), this,
4949 SLOT(ScreenshotScene()));
4950 popup.addSeparator();
4951 popup.addAction(QTStr("Filters"), this,
4952 SLOT(OpenSceneFilters()));
4953
4954 popup.addSeparator();
4955
4956 delete perSceneTransitionMenu;
4957 perSceneTransitionMenu = CreatePerSceneTransitionMenu();
4958 popup.addMenu(perSceneTransitionMenu);
4959
4960 /* ---------------------- */
4961
4962 QAction *multiviewAction =
4963 popup.addAction(QTStr("ShowInMultiview"));
4964
4965 OBSSource source = GetCurrentSceneSource();
4966 OBSData data = obs_source_get_private_settings(source);
4967 obs_data_release(data);
4968
4969 obs_data_set_default_bool(data, "show_in_multiview", true);
4970 bool show = obs_data_get_bool(data, "show_in_multiview");
4971
4972 multiviewAction->setCheckable(true);
4973 multiviewAction->setChecked(show);
4974
4975 auto showInMultiview = [](OBSData data) {
4976 bool show =
4977 obs_data_get_bool(data, "show_in_multiview");
4978 obs_data_set_bool(data, "show_in_multiview", !show);
4979 OBSProjector::UpdateMultiviewProjectors();
4980 };
4981
4982 connect(multiviewAction, &QAction::triggered,
4983 std::bind(showInMultiview, data));
4984
4985 copyFilters->setEnabled(obs_source_filter_count(source) > 0);
4986 }
4987
4988 popup.addSeparator();
4989
4990 bool grid = ui->scenes->GetGridMode();
4991
4992 QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode")
4993 : QTStr("Basic.Main.GridMode"),
4994 this);
4995 connect(gridAction, SIGNAL(triggered()), this,
4996 SLOT(GridActionClicked()));
4997 popup.addAction(gridAction);
4998
4999 popup.exec(QCursor::pos());
5000 }
5001
5002 void OBSBasic::GridActionClicked()
5003 {
5004 bool gridMode = !ui->scenes->GetGridMode();
5005 ui->scenes->SetGridMode(gridMode);
5006 }
5007
5008 void OBSBasic::on_actionAddScene_triggered()
5009 {
5010 string name;
5011 QString format{QTStr("Basic.Main.DefaultSceneName.Text")};
5012
5013 int i = 2;
5014 QString placeHolderText = format.arg(i);
5015 obs_source_t *source = nullptr;
5016 while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) {
5017 obs_source_release(source);
5018 placeHolderText = format.arg(++i);
5019 }
5020
5021 bool accepted = NameDialog::AskForName(
5022 this, QTStr("Basic.Main.AddSceneDlg.Title"),
5023 QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText);
5024
5025 if (accepted) {
5026 if (name.empty()) {
5027 OBSMessageBox::warning(this,
5028 QTStr("NoNameEntered.Title"),
5029 QTStr("NoNameEntered.Text"));
5030 on_actionAddScene_triggered();
5031 return;
5032 }
5033
5034 obs_source_t *source = obs_get_source_by_name(name.c_str());
5035 if (source) {
5036 OBSMessageBox::warning(this, QTStr("NameExists.Title"),
5037 QTStr("NameExists.Text"));
5038
5039 obs_source_release(source);
5040 on_actionAddScene_triggered();
5041 return;
5042 }
5043
5044 auto undo_fn = [](const std::string &data) {
5045 obs_source_t *t = obs_get_source_by_name(data.c_str());
5046 if (t) {
5047 obs_source_release(t);
5048 obs_source_remove(t);
5049 }
5050 };
5051
5052 auto redo_fn = [this](const std::string &data) {
5053 obs_scene_t *scene = obs_scene_create(data.c_str());
5054 obs_source_t *source = obs_scene_get_source(scene);
5055 SetCurrentScene(source, true);
5056 obs_scene_release(scene);
5057 };
5058 undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())),
5059 undo_fn, redo_fn, name, name);
5060
5061 obs_scene_t *scene = obs_scene_create(name.c_str());
5062 source = obs_scene_get_source(scene);
5063 SetCurrentScene(source);
5064 RefreshSources(scene);
5065 obs_scene_release(scene);
5066 }
5067 }
5068
5069 void OBSBasic::on_actionRemoveScene_triggered()
5070 {
5071 RemoveSelectedScene();
5072 }
5073
5074 void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx)
5075 {
5076 int idx = ui->scenes->currentRow();
5077 if (idx == -1 || idx == invalidIdx)
5078 return;
5079
5080 ui->scenes->blockSignals(true);
5081 QListWidgetItem *item = ui->scenes->takeItem(idx);
5082
5083 if (!relative)
5084 idx = 0;
5085
5086 ui->scenes->insertItem(idx + offset, item);
5087 ui->scenes->setCurrentRow(idx + offset);
5088 item->setSelected(true);
5089 ui->scenes->blockSignals(false);
5090
5091 OBSProjector::UpdateMultiviewProjectors();
5092 }
5093
5094 void OBSBasic::on_actionSceneUp_triggered()
5095 {
5096 ChangeSceneIndex(true, -1, 0);
5097 }
5098
5099 void OBSBasic::on_actionSceneDown_triggered()
5100 {
5101 ChangeSceneIndex(true, 1, ui->scenes->count() - 1);
5102 }
5103
5104 void OBSBasic::MoveSceneToTop()
5105 {
5106 ChangeSceneIndex(false, 0, 0);
5107 }
5108
5109 void OBSBasic::MoveSceneToBottom()
5110 {
5111 ChangeSceneIndex(false, ui->scenes->count() - 1,
5112 ui->scenes->count() - 1);
5113 }
5114
5115 void OBSBasic::EditSceneItemName()
5116 {
5117 int idx = GetTopSelectedSourceItem();
5118 ui->sources->Edit(idx);
5119 }
5120
5121 void OBSBasic::SetDeinterlacingMode()
5122 {
5123 QAction *action = reinterpret_cast<QAction *>(sender());
5124 obs_deinterlace_mode mode =
5125 (obs_deinterlace_mode)action->property("mode").toInt();
5126 OBSSceneItem sceneItem = GetCurrentSceneItem();
5127 obs_source_t *source = obs_sceneitem_get_source(sceneItem);
5128
5129 obs_source_set_deinterlace_mode(source, mode);
5130 }
5131
5132 void OBSBasic::SetDeinterlacingOrder()
5133 {
5134 QAction *action = reinterpret_cast<QAction *>(sender());
5135 obs_deinterlace_field_order order =
5136 (obs_deinterlace_field_order)action->property("order").toInt();
5137 OBSSceneItem sceneItem = GetCurrentSceneItem();
5138 obs_source_t *source = obs_sceneitem_get_source(sceneItem);
5139
5140 obs_source_set_deinterlace_field_order(source, order);
5141 }
5142
5143 QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source)
5144 {
5145 obs_deinterlace_mode deinterlaceMode =
5146 obs_source_get_deinterlace_mode(source);
5147 obs_deinterlace_field_order deinterlaceOrder =
5148 obs_source_get_deinterlace_field_order(source);
5149 QAction *action;
5150
5151 #define ADD_MODE(name, mode) \
5152 action = menu->addAction(QTStr("" name), this, \
5153 SLOT(SetDeinterlacingMode())); \
5154 action->setProperty("mode", (int)mode); \
5155 action->setCheckable(true); \
5156 action->setChecked(deinterlaceMode == mode);
5157
5158 ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE);
5159 ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD);
5160 ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO);
5161 ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND);
5162 ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X);
5163 ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR);
5164 ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X);
5165 ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF);
5166 ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X);
5167 #undef ADD_MODE
5168
5169 menu->addSeparator();
5170
5171 #define ADD_ORDER(name, order) \
5172 action = menu->addAction(QTStr("Deinterlacing." name), this, \
5173 SLOT(SetDeinterlacingOrder())); \
5174 action->setProperty("order", (int)order); \
5175 action->setCheckable(true); \
5176 action->setChecked(deinterlaceOrder == order);
5177
5178 ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP);
5179 ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM);
5180 #undef ADD_ORDER
5181
5182 return menu;
5183 }
5184
5185 void OBSBasic::SetScaleFilter()
5186 {
5187 QAction *action = reinterpret_cast<QAction *>(sender());
5188 obs_scale_type mode = (obs_scale_type)action->property("mode").toInt();
5189 OBSSceneItem sceneItem = GetCurrentSceneItem();
5190
5191 obs_sceneitem_set_scale_filter(sceneItem, mode);
5192 }
5193
5194 QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item)
5195 {
5196 obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item);
5197 QAction *action;
5198
5199 #define ADD_MODE(name, mode) \
5200 action = \
5201 menu->addAction(QTStr("" name), this, SLOT(SetScaleFilter())); \
5202 action->setProperty("mode", (int)mode); \
5203 action->setCheckable(true); \
5204 action->setChecked(scaleFilter == mode);
5205
5206 ADD_MODE("Disable", OBS_SCALE_DISABLE);
5207 ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT);
5208 ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR);
5209 ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC);
5210 ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS);
5211 ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA);
5212 #undef ADD_MODE
5213
5214 return menu;
5215 }
5216
5217 QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu,
5218 QWidgetAction *widgetAction,
5219 ColorSelect *select,
5220 obs_sceneitem_t *item)
5221 {
5222 QAction *action;
5223
5224 menu->setStyleSheet(QString(
5225 "*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}"
5226 "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}"
5227 "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}"
5228 "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}"
5229 "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}"
5230 "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}"
5231 "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}"
5232 "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}"));
5233
5234 obs_data_t *privData = obs_sceneitem_get_private_settings(item);
5235 obs_data_release(privData);
5236
5237 obs_data_set_default_int(privData, "color-preset", 0);
5238 int preset = obs_data_get_int(privData, "color-preset");
5239
5240 action = menu->addAction(QTStr("Clear"), this, +SLOT(ColorChange()));
5241 action->setCheckable(true);
5242 action->setProperty("bgColor", 0);
5243 action->setChecked(preset == 0);
5244
5245 action = menu->addAction(QTStr("CustomColor"), this,
5246 +SLOT(ColorChange()));
5247 action->setCheckable(true);
5248 action->setProperty("bgColor", 1);
5249 action->setChecked(preset == 1);
5250
5251 menu->addSeparator();
5252
5253 widgetAction->setDefaultWidget(select);
5254
5255 for (int i = 1; i < 9; i++) {
5256 stringstream button;
5257 button << "preset" << i;
5258 QPushButton *colorButton =
5259 select->findChild<QPushButton *>(button.str().c_str());
5260 if (preset == i + 1)
5261 colorButton->setStyleSheet("border: 2px solid black");
5262
5263 colorButton->setProperty("bgColor", i);
5264 select->connect(colorButton, SIGNAL(released()), this,
5265 SLOT(ColorChange()));
5266 }
5267
5268 menu->addAction(widgetAction);
5269
5270 return menu;
5271 }
5272
5273 ColorSelect::ColorSelect(QWidget *parent)
5274 : QWidget(parent), ui(new Ui::ColorSelect)
5275 {
5276 ui->setupUi(this);
5277 }
5278
5279 void OBSBasic::CreateSourcePopupMenu(int idx, bool preview)
5280 {
5281 QMenu popup(this);
5282 delete previewProjectorSource;
5283 delete sourceProjector;
5284 delete scaleFilteringMenu;
5285 delete colorMenu;
5286 delete colorWidgetAction;
5287 delete colorSelect;
5288 delete deinterlaceMenu;
5289
5290 if (preview) {
5291 QAction *action = popup.addAction(
5292 QTStr("Basic.Main.PreviewConextMenu.Enable"), this,
5293 SLOT(TogglePreview()));
5294 action->setCheckable(true);
5295 action->setChecked(
5296 obs_display_enabled(ui->preview->GetDisplay()));
5297 if (IsPreviewProgramMode())
5298 action->setEnabled(false);
5299
5300 popup.addAction(ui->actionLockPreview);
5301 popup.addMenu(ui->scalingMenu);
5302
5303 previewProjectorSource = new QMenu(QTStr("PreviewProjector"));
5304 AddProjectorMenuMonitors(previewProjectorSource, this,
5305 SLOT(OpenPreviewProjector()));
5306
5307 popup.addMenu(previewProjectorSource);
5308
5309 QAction *previewWindow =
5310 popup.addAction(QTStr("PreviewWindow"), this,
5311 SLOT(OpenPreviewWindow()));
5312
5313 popup.addAction(previewWindow);
5314
5315 popup.addAction(QTStr("Screenshot.Preview"), this,
5316 SLOT(ScreenshotScene()));
5317
5318 popup.addSeparator();
5319 }
5320
5321 QPointer<QMenu> addSourceMenu = CreateAddSourcePopupMenu();
5322 if (addSourceMenu)
5323 popup.addMenu(addSourceMenu);
5324
5325 ui->actionCopyFilters->setEnabled(false);
5326 ui->actionCopySource->setEnabled(false);
5327
5328 if (ui->sources->MultipleBaseSelected()) {
5329 popup.addSeparator();
5330 popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources,
5331 SLOT(GroupSelectedItems()));
5332
5333 } else if (ui->sources->GroupsSelected()) {
5334 popup.addSeparator();
5335 popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources,
5336 SLOT(UngroupSelectedGroups()));
5337 }
5338
5339 popup.addSeparator();
5340 popup.addAction(ui->actionCopySource);
5341 popup.addAction(ui->actionPasteRef);
5342 popup.addAction(ui->actionPasteDup);
5343 popup.addSeparator();
5344
5345 popup.addSeparator();
5346 popup.addAction(ui->actionCopyFilters);
5347 popup.addAction(ui->actionPasteFilters);
5348 popup.addSeparator();
5349
5350 if (idx != -1) {
5351 if (addSourceMenu)
5352 popup.addSeparator();
5353
5354 OBSSceneItem sceneItem = ui->sources->Get(idx);
5355 bool lock = obs_sceneitem_locked(sceneItem);
5356 obs_source_t *source = obs_sceneitem_get_source(sceneItem);
5357 uint32_t flags = obs_source_get_output_flags(source);
5358 bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) ==
5359 OBS_SOURCE_ASYNC_VIDEO;
5360 bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO;
5361 QAction *action;
5362
5363 colorMenu = new QMenu(QTStr("ChangeBG"));
5364 colorWidgetAction = new QWidgetAction(colorMenu);
5365 colorSelect = new ColorSelect(colorMenu);
5366 popup.addMenu(AddBackgroundColorMenu(
5367 colorMenu, colorWidgetAction, colorSelect, sceneItem));
5368 popup.addAction(QTStr("Rename"), this,
5369 SLOT(EditSceneItemName()));
5370 popup.addAction(QTStr("Remove"), this,
5371 SLOT(on_actionRemoveSource_triggered()));
5372 popup.addSeparator();
5373 popup.addMenu(ui->orderMenu);
5374 popup.addMenu(ui->transformMenu);
5375
5376 ui->actionResetTransform->setEnabled(!lock);
5377 ui->actionRotate90CW->setEnabled(!lock);
5378 ui->actionRotate90CCW->setEnabled(!lock);
5379 ui->actionRotate180->setEnabled(!lock);
5380 ui->actionFlipHorizontal->setEnabled(!lock);
5381 ui->actionFlipVertical->setEnabled(!lock);
5382 ui->actionFitToScreen->setEnabled(!lock);
5383 ui->actionStretchToScreen->setEnabled(!lock);
5384 ui->actionCenterToScreen->setEnabled(!lock);
5385 ui->actionVerticalCenter->setEnabled(!lock);
5386 ui->actionHorizontalCenter->setEnabled(!lock);
5387
5388 sourceProjector = new QMenu(QTStr("SourceProjector"));
5389 AddProjectorMenuMonitors(sourceProjector, this,
5390 SLOT(OpenSourceProjector()));
5391
5392 QAction *sourceWindow = popup.addAction(
5393 QTStr("SourceWindow"), this, SLOT(OpenSourceWindow()));
5394
5395 popup.addAction(sourceWindow);
5396
5397 popup.addSeparator();
5398
5399 if (hasAudio) {
5400 QAction *actionHideMixer =
5401 popup.addAction(QTStr("HideMixer"), this,
5402 SLOT(ToggleHideMixer()));
5403 actionHideMixer->setCheckable(true);
5404 actionHideMixer->setChecked(SourceMixerHidden(source));
5405 }
5406
5407 if (isAsyncVideo) {
5408 deinterlaceMenu = new QMenu(QTStr("Deinterlacing"));
5409 popup.addMenu(
5410 AddDeinterlacingMenu(deinterlaceMenu, source));
5411 popup.addSeparator();
5412 }
5413
5414 QAction *resizeOutput =
5415 popup.addAction(QTStr("ResizeOutputSizeOfSource"), this,
5416 SLOT(ResizeOutputSizeOfSource()));
5417
5418 int width = obs_source_get_width(source);
5419 int height = obs_source_get_height(source);
5420
5421 resizeOutput->setEnabled(!obs_video_active());
5422
5423 if (width < 8 || height < 8)
5424 resizeOutput->setEnabled(false);
5425
5426 scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering"));
5427 popup.addMenu(
5428 AddScaleFilteringMenu(scaleFilteringMenu, sceneItem));
5429 popup.addSeparator();
5430
5431 popup.addMenu(sourceProjector);
5432 popup.addAction(sourceWindow);
5433 popup.addAction(QTStr("Screenshot.Source"), this,
5434 SLOT(ScreenshotSelectedSource()));
5435 popup.addSeparator();
5436
5437 popup.addMenu(CreateVisibilityTransitionMenu(true));
5438 popup.addMenu(CreateVisibilityTransitionMenu(false));
5439 popup.addSeparator();
5440
5441 action = popup.addAction(QTStr("Interact"), this,
5442 SLOT(on_actionInteract_triggered()));
5443
5444 action->setEnabled(obs_source_get_output_flags(source) &
5445 OBS_SOURCE_INTERACTION);
5446
5447 popup.addAction(QTStr("Filters"), this, SLOT(OpenFilters()));
5448 popup.addAction(QTStr("Properties"), this,
5449 SLOT(on_actionSourceProperties_triggered()));
5450
5451 ui->actionCopyFilters->setEnabled(
5452 obs_source_filter_count(source) > 0);
5453 ui->actionCopySource->setEnabled(true);
5454 }
5455 ui->actionPasteFilters->setEnabled(copyFiltersString && idx != -1);
5456
5457 popup.exec(QCursor::pos());
5458 }
5459
5460 void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos)
5461 {
5462 if (ui->scenes->count()) {
5463 QModelIndex idx = ui->sources->indexAt(pos);
5464 CreateSourcePopupMenu(idx.row(), false);
5465 }
5466 }
5467
5468 void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem)
5469 {
5470 if (!witem)
5471 return;
5472
5473 if (IsPreviewProgramMode()) {
5474 bool doubleClickSwitch =
5475 config_get_bool(App()->GlobalConfig(), "BasicWindow",
5476 "TransitionOnDoubleClick");
5477
5478 if (doubleClickSwitch)
5479 TransitionClicked();
5480 }
5481 }
5482
5483 void OBSBasic::AddSource(const char *id)
5484 {
5485 if (id && *id) {
5486 OBSBasicSourceSelect sourceSelect(this, id, undo_s);
5487 sourceSelect.exec();
5488 if (sourceSelect.newSource && strcmp(id, "group") != 0) {
5489 CreatePropertiesWindow(sourceSelect.newSource);
5490 }
5491 }
5492 }
5493
5494 QMenu *OBSBasic::CreateAddSourcePopupMenu()
5495 {
5496 const char *unversioned_type;
5497 const char *type;
5498 bool foundValues = false;
5499 bool foundDeprecated = false;
5500 size_t idx = 0;
5501
5502 QMenu *popup = new QMenu(QTStr("Add"), this);
5503 QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup);
5504
5505 auto getActionAfter = [](QMenu *menu, const QString &name) {
5506 QList<QAction *> actions = menu->actions();
5507
5508 for (QAction *menuAction : actions) {
5509 if (menuAction->text().compare(name) >= 0)
5510 return menuAction;
5511 }
5512
5513 return (QAction *)nullptr;
5514 };
5515
5516 auto addSource = [this, getActionAfter](QMenu *popup, const char *type,
5517 const char *name) {
5518 QString qname = QT_UTF8(name);
5519 QAction *popupItem = new QAction(qname, this);
5520 popupItem->setData(QT_UTF8(type));
5521 connect(popupItem, SIGNAL(triggered(bool)), this,
5522 SLOT(AddSourceFromAction()));
5523
5524 QIcon icon;
5525
5526 if (strcmp(type, "scene") == 0)
5527 icon = GetSceneIcon();
5528 else
5529 icon = GetSourceIcon(type);
5530
5531 popupItem->setIcon(icon);
5532
5533 QAction *after = getActionAfter(popup, qname);
5534 popup->insertAction(after, popupItem);
5535 };
5536
5537 while (obs_enum_input_types2(idx++, &type, &unversioned_type)) {
5538 const char *name = obs_source_get_display_name(type);
5539 uint32_t caps = obs_get_source_output_flags(type);
5540
5541 if ((caps & OBS_SOURCE_CAP_DISABLED) != 0)
5542 continue;
5543
5544 if ((caps & OBS_SOURCE_DEPRECATED) == 0) {
5545 addSource(popup, unversioned_type, name);
5546 } else {
5547 addSource(deprecated, unversioned_type, name);
5548 foundDeprecated = true;
5549 }
5550 foundValues = true;
5551 }
5552
5553 addSource(popup, "scene", Str("Basic.Scene"));
5554
5555 popup->addSeparator();
5556 QAction *addGroup = new QAction(QTStr("Group"), this);
5557 addGroup->setData(QT_UTF8("group"));
5558 addGroup->setIcon(GetGroupIcon());
5559 connect(addGroup, SIGNAL(triggered(bool)), this,
5560 SLOT(AddSourceFromAction()));
5561 popup->addAction(addGroup);
5562
5563 if (!foundDeprecated) {
5564 delete deprecated;
5565 deprecated = nullptr;
5566 }
5567
5568 if (!foundValues) {
5569 delete popup;
5570 popup = nullptr;
5571
5572 } else if (foundDeprecated) {
5573 popup->addSeparator();
5574 popup->addMenu(deprecated);
5575 }
5576
5577 return popup;
5578 }
5579
5580 void OBSBasic::AddSourceFromAction()
5581 {
5582 QAction *action = qobject_cast<QAction *>(sender());
5583 if (!action)
5584 return;
5585
5586 AddSource(QT_TO_UTF8(action->data().toString()));
5587 }
5588
5589 void OBSBasic::AddSourcePopupMenu(const QPoint &pos)
5590 {
5591 if (!GetCurrentScene()) {
5592 // Tell the user he needs a scene first (help beginners).
5593 OBSMessageBox::information(
5594 this, QTStr("Basic.Main.AddSourceHelp.Title"),
5595 QTStr("Basic.Main.AddSourceHelp.Text"));
5596 return;
5597 }
5598
5599 QScopedPointer<QMenu> popup(CreateAddSourcePopupMenu());
5600 if (popup)
5601 popup->exec(pos);
5602 }
5603
5604 void OBSBasic::on_actionAddSource_triggered()
5605 {
5606 AddSourcePopupMenu(QCursor::pos());
5607 }
5608
5609 static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param)
5610 {
5611 vector<OBSSceneItem> &items =
5612 *reinterpret_cast<vector<OBSSceneItem> *>(param);
5613
5614 if (obs_sceneitem_selected(item)) {
5615 items.emplace_back(item);
5616 } else if (obs_sceneitem_is_group(item)) {
5617 obs_sceneitem_group_enum_items(item, remove_items, &items);
5618 }
5619 return true;
5620 };
5621
5622 OBSData OBSBasic::BackupScene(obs_scene_t *scene,
5623 std::vector<obs_source_t *> *sources)
5624 {
5625 obs_data_array_t *undo_array = obs_data_array_create();
5626
5627 if (!sources) {
5628 obs_scene_enum_items(scene, save_undo_source_enum, undo_array);
5629 } else {
5630 for (obs_source_t *source : *sources) {
5631 obs_data_t *source_data = obs_save_source(source);
5632 obs_data_array_push_back(undo_array, source_data);
5633 obs_data_release(source_data);
5634 }
5635 }
5636
5637 obs_data_t *scene_data = obs_save_source(obs_scene_get_source(scene));
5638 obs_data_array_push_back(undo_array, scene_data);
5639 obs_data_release(scene_data);
5640
5641 OBSData data = obs_data_create();
5642 obs_data_release(data);
5643
5644 obs_data_set_array(data, "array", undo_array);
5645 obs_data_get_json(data);
5646 obs_data_array_release(undo_array);
5647 return data;
5648 }
5649
5650 static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p)
5651 {
5652 auto sources = static_cast<std::vector<obs_source_t *> *>(p);
5653 sources->push_back(obs_sceneitem_get_source(item));
5654 return true;
5655 }
5656
5657 void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name,
5658 OBSData undo_data, OBSData redo_data)
5659 {
5660 auto undo_redo = [this](const std::string &json) {
5661 obs_data_t *base = obs_data_create_from_json(json.c_str());
5662 obs_data_array_t *array = obs_data_get_array(base, "array");
5663 std::vector<obs_source_t *> sources;
5664 std::vector<obs_source_t *> old_sources;
5665
5666 /* create missing sources */
5667 const size_t count = obs_data_array_count(array);
5668 sources.reserve(count);
5669
5670 for (size_t i = 0; i < count; i++) {
5671 obs_data_t *data = obs_data_array_item(array, i);
5672 const char *name = obs_data_get_string(data, "name");
5673
5674 obs_source_t *source = obs_get_source_by_name(name);
5675 if (!source)
5676 source = obs_load_source(data);
5677
5678 sources.push_back(source);
5679
5680 /* update scene/group settings to restore their
5681 * contents to their saved settings */
5682 obs_scene_t *scene =
5683 obs_group_or_scene_from_source(source);
5684 if (scene) {
5685 obs_scene_enum_items(scene, add_source_enum,
5686 &old_sources);
5687 obs_data_t *scene_settings =
5688 obs_data_get_obj(data, "settings");
5689 obs_source_update(source, scene_settings);
5690 obs_data_release(scene_settings);
5691 }
5692
5693 obs_data_release(data);
5694 }
5695 for (obs_source_t *source : old_sources)
5696 obs_source_addref(source);
5697
5698 /* actually load sources now */
5699 for (obs_source_t *source : sources)
5700 obs_source_load2(source);
5701
5702 /* release sources */
5703 for (obs_source_t *source : sources)
5704 obs_source_release(source);
5705 for (obs_source_t *source : old_sources)
5706 obs_source_release(source);
5707
5708 obs_data_array_release(array);
5709 obs_data_release(base);
5710
5711 ui->sources->RefreshItems();
5712 };
5713
5714 const char *undo_json = obs_data_get_last_json(undo_data);
5715 const char *redo_json = obs_data_get_last_json(redo_data);
5716
5717 undo_s.add_action(action_name, undo_redo, undo_redo, undo_json,
5718 redo_json);
5719 }
5720
5721 void OBSBasic::on_actionRemoveSource_triggered()
5722 {
5723 vector<OBSSceneItem> items;
5724 OBSScene scene = GetCurrentScene();
5725 obs_source_t *scene_source = obs_scene_get_source(scene);
5726
5727 obs_scene_enum_items(scene, remove_items, &items);
5728
5729 if (!items.size())
5730 return;
5731
5732 /* ------------------------------------- */
5733 /* confirm action with user */
5734
5735 bool confirmed = false;
5736
5737 if (items.size() > 1) {
5738 QString text = QTStr("ConfirmRemove.TextMultiple")
5739 .arg(QString::number(items.size()));
5740
5741 QMessageBox remove_items(this);
5742 remove_items.setText(text);
5743 QPushButton *Yes = remove_items.addButton(QTStr("Yes"),
5744 QMessageBox::YesRole);
5745 remove_items.setDefaultButton(Yes);
5746 remove_items.addButton(QTStr("No"), QMessageBox::NoRole);
5747 remove_items.setIcon(QMessageBox::Question);
5748 remove_items.setWindowTitle(QTStr("ConfirmRemove.Title"));
5749 remove_items.exec();
5750
5751 confirmed = Yes == remove_items.clickedButton();
5752 } else {
5753 OBSSceneItem &item = items[0];
5754 obs_source_t *source = obs_sceneitem_get_source(item);
5755 if (source && QueryRemoveSource(source))
5756 confirmed = true;
5757 }
5758 if (!confirmed)
5759 return;
5760
5761 /* ----------------------------------------------- */
5762 /* save undo data */
5763
5764 OBSData undo_data = BackupScene(scene_source);
5765
5766 /* ----------------------------------------------- */
5767 /* remove items */
5768
5769 for (auto &item : items)
5770 obs_sceneitem_remove(item);
5771
5772 /* ----------------------------------------------- */
5773 /* save redo data */
5774
5775 OBSData redo_data = BackupScene(scene_source);
5776
5777 /* ----------------------------------------------- */
5778 /* add undo/redo action */
5779
5780 QString action_name;
5781 if (items.size() > 1) {
5782 action_name = QTStr("Undo.Sources.Multi")
5783 .arg(QString::number(items.size()));
5784 } else {
5785 QString str = QTStr("Undo.Delete");
5786 action_name = str.arg(obs_source_get_name(
5787 obs_sceneitem_get_source(items[0])));
5788 }
5789
5790 CreateSceneUndoRedoAction(action_name, undo_data, redo_data);
5791 }
5792
5793 void OBSBasic::on_actionInteract_triggered()
5794 {
5795 OBSSceneItem item = GetCurrentSceneItem();
5796 OBSSource source = obs_sceneitem_get_source(item);
5797
5798 if (source)
5799 CreateInteractionWindow(source);
5800 }
5801
5802 void OBSBasic::on_actionSourceProperties_triggered()
5803 {
5804 OBSSceneItem item = GetCurrentSceneItem();
5805 OBSSource source = obs_sceneitem_get_source(item);
5806
5807 if (source)
5808 CreatePropertiesWindow(source);
5809 }
5810
5811 void OBSBasic::MoveSceneItem(enum obs_order_movement movement,
5812 const QString &action_name)
5813 {
5814 OBSSceneItem item = GetCurrentSceneItem();
5815 obs_source_t *source = obs_sceneitem_get_source(item);
5816
5817 if (!source)
5818 return;
5819
5820 OBSScene scene = GetCurrentScene();
5821 std::vector<obs_source_t *> sources;
5822 if (scene != obs_sceneitem_get_scene(item))
5823 sources.push_back(
5824 obs_scene_get_source(obs_sceneitem_get_scene(item)));
5825
5826 OBSData undo_data = BackupScene(scene, &sources);
5827
5828 obs_sceneitem_set_order(item, movement);
5829
5830 const char *source_name = obs_source_get_name(source);
5831 const char *scene_name =
5832 obs_source_get_name(obs_scene_get_source(scene));
5833
5834 OBSData redo_data = BackupScene(scene, &sources);
5835 CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name),
5836 undo_data, redo_data);
5837 }
5838
5839 void OBSBasic::on_actionSourceUp_triggered()
5840 {
5841 MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp"));
5842 }
5843
5844 void OBSBasic::on_actionSourceDown_triggered()
5845 {
5846 MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown"));
5847 }
5848
5849 void OBSBasic::on_actionMoveUp_triggered()
5850 {
5851 MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp"));
5852 }
5853
5854 void OBSBasic::on_actionMoveDown_triggered()
5855 {
5856 MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown"));
5857 }
5858
5859 void OBSBasic::on_actionMoveToTop_triggered()
5860 {
5861 MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop"));
5862 }
5863
5864 void OBSBasic::on_actionMoveToBottom_triggered()
5865 {
5866 MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom"));
5867 }
5868
5869 static BPtr<char> ReadLogFile(const char *subdir, const char *log)
5870 {
5871 char logDir[512];
5872 if (GetConfigPath(logDir, sizeof(logDir), subdir) <= 0)
5873 return nullptr;
5874
5875 string path = logDir;
5876 path += "/";
5877 path += log;
5878
5879 BPtr<char> file = os_quick_read_utf8_file(path.c_str());
5880 if (!file)
5881 blog(LOG_WARNING, "Failed to read log file %s", path.c_str());
5882
5883 return file;
5884 }
5885
5886 void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash)
5887 {
5888 BPtr<char> fileString{ReadLogFile(subdir, file)};
5889
5890 if (!fileString)
5891 return;
5892
5893 if (!*fileString)
5894 return;
5895
5896 ui->menuLogFiles->setEnabled(false);
5897 #if defined(_WIN32)
5898 ui->menuCrashLogs->setEnabled(false);
5899 #endif
5900
5901 stringstream ss;
5902 ss << "OBS " << App()->GetVersionString() << " log file uploaded at "
5903 << CurrentDateTimeString() << "\n\n"
5904 << fileString;
5905
5906 if (logUploadThread) {
5907 logUploadThread->wait();
5908 }
5909
5910 RemoteTextThread *thread =
5911 new RemoteTextThread("https://obsproject.com/logs/upload",
5912 "text/plain", ss.str().c_str());
5913
5914 logUploadThread.reset(thread);
5915 if (crash) {
5916 connect(thread, &RemoteTextThread::Result, this,
5917 &OBSBasic::crashUploadFinished);
5918 } else {
5919 connect(thread, &RemoteTextThread::Result, this,
5920 &OBSBasic::logUploadFinished);
5921 }
5922 logUploadThread->start();
5923 }
5924
5925 void OBSBasic::on_actionShowLogs_triggered()
5926 {
5927 char logDir[512];
5928 if (GetConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0)
5929 return;
5930
5931 QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir));
5932 QDesktopServices::openUrl(url);
5933 }
5934
5935 void OBSBasic::on_actionUploadCurrentLog_triggered()
5936 {
5937 UploadLog("obs-studio/logs", App()->GetCurrentLog(), false);
5938 }
5939
5940 void OBSBasic::on_actionUploadLastLog_triggered()
5941 {
5942 UploadLog("obs-studio/logs", App()->GetLastLog(), false);
5943 }
5944
5945 void OBSBasic::on_actionViewCurrentLog_triggered()
5946 {
5947 if (!logView)
5948 logView = new OBSLogViewer();
5949
5950 logView->show();
5951 logView->setWindowState(
5952 (logView->windowState() & ~Qt::WindowMinimized) |
5953 Qt::WindowActive);
5954 logView->activateWindow();
5955 logView->raise();
5956 }
5957
5958 void OBSBasic::on_actionShowCrashLogs_triggered()
5959 {
5960 char logDir[512];
5961 if (GetConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0)
5962 return;
5963
5964 QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir));
5965 QDesktopServices::openUrl(url);
5966 }
5967
5968 void OBSBasic::on_actionUploadLastCrashLog_triggered()
5969 {
5970 UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true);
5971 }
5972
5973 void OBSBasic::on_actionCheckForUpdates_triggered()
5974 {
5975 CheckForUpdates(true);
5976 }
5977
5978 void OBSBasic::logUploadFinished(const QString &text, const QString &error)
5979 {
5980 ui->menuLogFiles->setEnabled(true);
5981 #if defined(_WIN32)
5982 ui->menuCrashLogs->setEnabled(true);
5983 #endif
5984
5985 if (text.isEmpty()) {
5986 OBSMessageBox::critical(
5987 this, QTStr("LogReturnDialog.ErrorUploadingLog"),
5988 error);
5989 return;
5990 }
5991 openLogDialog(text, false);
5992 }
5993
5994 void OBSBasic::crashUploadFinished(const QString &text, const QString &error)
5995 {
5996 ui->menuLogFiles->setEnabled(true);
5997 #if defined(_WIN32)
5998 ui->menuCrashLogs->setEnabled(true);
5999 #endif
6000
6001 if (text.isEmpty()) {
6002 OBSMessageBox::critical(
6003 this, QTStr("LogReturnDialog.ErrorUploadingLog"),
6004 error);
6005 return;
6006 }
6007 openLogDialog(text, true);
6008 }
6009
6010 void OBSBasic::openLogDialog(const QString &text, const bool crash)
6011 {
6012
6013 obs_data_t *returnData = obs_data_create_from_json(QT_TO_UTF8(text));
6014 string resURL = obs_data_get_string(returnData, "url");
6015 QString logURL = resURL.c_str();
6016 obs_data_release(returnData);
6017
6018 OBSLogReply logDialog(this, logURL, crash);
6019 logDialog.exec();
6020 }
6021
6022 static void RenameListItem(OBSBasic *parent, QListWidget *listWidget,
6023 obs_source_t *source, const string &name)
6024 {
6025 const char *prevName = obs_source_get_name(source);
6026 if (name == prevName)
6027 return;
6028
6029 obs_source_t *foundSource = obs_get_source_by_name(name.c_str());
6030 QListWidgetItem *listItem = listWidget->currentItem();
6031
6032 if (foundSource || name.empty()) {
6033 listItem->setText(QT_UTF8(prevName));
6034
6035 if (foundSource) {
6036 OBSMessageBox::warning(parent,
6037 QTStr("NameExists.Title"),
6038 QTStr("NameExists.Text"));
6039 } else if (name.empty()) {
6040 OBSMessageBox::warning(parent,
6041 QTStr("NoNameEntered.Title"),
6042 QTStr("NoNameEntered.Text"));
6043 }
6044
6045 obs_source_release(foundSource);
6046 } else {
6047 auto undo = [prev = std::string(prevName)](
6048 const std::string &data) {
6049 obs_source_t *source =
6050 obs_get_source_by_name(data.c_str());
6051 obs_source_set_name(source, prev.c_str());
6052 obs_source_release(source);
6053 };
6054
6055 auto redo = [name](const std::string &data) {
6056 obs_source_t *source =
6057 obs_get_source_by_name(data.c_str());
6058 obs_source_set_name(source, name.c_str());
6059 obs_source_release(source);
6060 };
6061
6062 std::string undo_data(name);
6063 std::string redo_data(prevName);
6064 parent->undo_s.add_action(
6065 QTStr("Undo.Rename").arg(name.c_str()), undo, redo,
6066 undo_data, redo_data);
6067
6068 listItem->setText(QT_UTF8(name.c_str()));
6069 obs_source_set_name(source, name.c_str());
6070 }
6071 }
6072
6073 void OBSBasic::SceneNameEdited(QWidget *editor,
6074 QAbstractItemDelegate::EndEditHint endHint)
6075 {
6076 OBSScene scene = GetCurrentScene();
6077 QLineEdit *edit = qobject_cast<QLineEdit *>(editor);
6078 string text = QT_TO_UTF8(edit->text().trimmed());
6079
6080 if (!scene)
6081 return;
6082
6083 obs_source_t *source = obs_scene_get_source(scene);
6084 RenameListItem(this, ui->scenes, source, text);
6085
6086 ui->scenesDock->addAction(renameScene);
6087
6088 if (api)
6089 api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
6090
6091 UNUSED_PARAMETER(endHint);
6092 }
6093
6094 void OBSBasic::OpenFilters(OBSSource source)
6095 {
6096 if (source == nullptr) {
6097 OBSSceneItem item = GetCurrentSceneItem();
6098 source = obs_sceneitem_get_source(item);
6099 }
6100 CreateFiltersWindow(source);
6101 }
6102
6103 void OBSBasic::OpenProperties(OBSSource source)
6104 {
6105 if (source == nullptr) {
6106 OBSSceneItem item = GetCurrentSceneItem();
6107 source = obs_sceneitem_get_source(item);
6108 }
6109 CreatePropertiesWindow(source);
6110 }
6111
6112 void OBSBasic::OpenSceneFilters()
6113 {
6114 OBSScene scene = GetCurrentScene();
6115 OBSSource source = obs_scene_get_source(scene);
6116
6117 CreateFiltersWindow(source);
6118 }
6119
6120 #define RECORDING_START \
6121 "==== Recording Start ==============================================="
6122 #define RECORDING_STOP \
6123 "==== Recording Stop ================================================"
6124 #define REPLAY_BUFFER_START \
6125 "==== Replay Buffer Start ==========================================="
6126 #define REPLAY_BUFFER_STOP \
6127 "==== Replay Buffer Stop ============================================"
6128 #define STREAMING_START \
6129 "==== Streaming Start ==============================================="
6130 #define STREAMING_STOP \
6131 "==== Streaming Stop ================================================"
6132 #define VIRTUAL_CAM_START \
6133 "==== Virtual Camera Start =========================================="
6134 #define VIRTUAL_CAM_STOP \
6135 "==== Virtual Camera Stop ==========================================="
6136
6137 void OBSBasic::DisplayStreamStartError()
6138 {
6139 QString message = !outputHandler->lastError.empty()
6140 ? QTStr(outputHandler->lastError.c_str())
6141 : QTStr("Output.StartFailedGeneric");
6142 ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
6143 ui->streamButton->setEnabled(true);
6144 ui->streamButton->setChecked(false);
6145
6146 if (sysTrayStream) {
6147 sysTrayStream->setText(ui->streamButton->text());
6148 sysTrayStream->setEnabled(true);
6149 }
6150
6151 QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message);
6152 }
6153
6154 #if YOUTUBE_ENABLED
6155 void OBSBasic::YouTubeActionDialogOk(const QString &id, const QString &key,
6156 bool autostart, bool autostop,
6157 bool start_now)
6158 {
6159 //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key));
6160 obs_service_t *service_obj = GetService();
6161 obs_data_t *settings = obs_service_get_settings(service_obj);
6162
6163 const std::string a_key = QT_TO_UTF8(key);
6164 obs_data_set_string(settings, "key", a_key.c_str());
6165
6166 const std::string an_id = QT_TO_UTF8(id);
6167 obs_data_set_string(settings, "stream_id", an_id.c_str());
6168
6169 obs_service_update(service_obj, settings);
6170 autoStartBroadcast = autostart;
6171 autoStopBroadcast = autostop;
6172 broadcastReady = true;
6173
6174 obs_data_release(settings);
6175
6176 if (start_now)
6177 QMetaObject::invokeMethod(this, "StartStreaming");
6178 }
6179
6180 void OBSBasic::YoutubeStreamCheck(const std::string &key)
6181 {
6182 YoutubeApiWrappers *apiYouTube(
6183 dynamic_cast<YoutubeApiWrappers *>(GetAuth()));
6184 if (!apiYouTube) {
6185 /* technically we should never get here -Jim */
6186 QMetaObject::invokeMethod(this, "ForceStopStreaming",
6187 Qt::QueuedConnection);
6188 youtubeStreamCheckThread->deleteLater();
6189 blog(LOG_ERROR, "==========================================");
6190 blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__);
6191 blog(LOG_ERROR, "==========================================");
6192 return;
6193 }
6194
6195 int timeout = 0;
6196 json11::Json json;
6197 QString id = key.c_str();
6198
6199 while (StreamingActive()) {
6200 if (timeout == 14) {
6201 QMetaObject::invokeMethod(this, "ForceStopStreaming",
6202 Qt::QueuedConnection);
6203 break;
6204 }
6205
6206 if (!apiYouTube->FindStream(id, json)) {
6207 QMetaObject::invokeMethod(this,
6208 "DisplayStreamStartError",
6209 Qt::QueuedConnection);
6210 QMetaObject::invokeMethod(this, "StopStreaming",
6211 Qt::QueuedConnection);
6212 break;
6213 }
6214
6215 auto item = json["items"][0];
6216 auto status = item["status"]["streamStatus"].string_value();
6217 if (status == "active") {
6218 QMetaObject::invokeMethod(ui->broadcastButton,
6219 "setEnabled",
6220 Q_ARG(bool, true));
6221 break;
6222 } else {
6223 QThread::sleep(1);
6224 timeout++;
6225 }
6226 }
6227
6228 youtubeStreamCheckThread->deleteLater();
6229 }
6230
6231 void OBSBasic::ShowYouTubeAutoStartWarning()
6232 {
6233 auto msgBox = []() {
6234 QMessageBox msgbox(App()->GetMainWindow());
6235 msgbox.setWindowTitle(QTStr(
6236 "YouTube.Actions.AutoStartStreamingWarning.Title"));
6237 msgbox.setText(
6238 QTStr("YouTube.Actions.AutoStartStreamingWarning"));
6239 msgbox.setIcon(QMessageBox::Icon::Information);
6240 msgbox.addButton(QMessageBox::Ok);
6241
6242 QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain"));
6243 msgbox.setCheckBox(cb);
6244
6245 msgbox.exec();
6246
6247 if (cb->isChecked()) {
6248 config_set_bool(App()->GlobalConfig(), "General",
6249 "WarnedAboutYouTubeAutoStart", true);
6250 config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
6251 }
6252 };
6253
6254 bool warned = config_get_bool(App()->GlobalConfig(), "General",
6255 "WarnedAboutYouTubeAutoStart");
6256 if (!warned) {
6257 QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection,
6258 Q_ARG(VoidFunc, msgBox));
6259 }
6260 }
6261 #endif
6262
6263 void OBSBasic::StartStreaming()
6264 {
6265 if (outputHandler->StreamingActive())
6266 return;
6267 if (disableOutputsRef)
6268 return;
6269
6270 if (auth && auth->broadcastFlow()) {
6271 if (!broadcastActive && !broadcastReady) {
6272 ui->streamButton->setChecked(false);
6273
6274 QMessageBox no_broadcast(this);
6275 no_broadcast.setText(QTStr("Output.NoBroadcast.Text"));
6276 QPushButton *SetupBroadcast = no_broadcast.addButton(
6277 QTStr("Basic.Main.SetupBroadcast"),
6278 QMessageBox::YesRole);
6279 no_broadcast.setDefaultButton(SetupBroadcast);
6280 no_broadcast.addButton(QTStr("Close"),
6281 QMessageBox::NoRole);
6282 no_broadcast.setIcon(QMessageBox::Information);
6283 no_broadcast.setWindowTitle(
6284 QTStr("Output.NoBroadcast.Title"));
6285 no_broadcast.exec();
6286
6287 if (no_broadcast.clickedButton() == SetupBroadcast)
6288 QMetaObject::invokeMethod(this,
6289 "SetupBroadcast");
6290 return;
6291 }
6292 }
6293
6294 if (!outputHandler->SetupStreaming(service)) {
6295 DisplayStreamStartError();
6296 return;
6297 }
6298
6299 if (api)
6300 api->on_event(OBS_FRONTEND_EVENT_STREAMING_STARTING);
6301
6302 SaveProject();
6303
6304 ui->streamButton->setEnabled(false);
6305 ui->streamButton->setChecked(false);
6306 ui->streamButton->setText(QTStr("Basic.Main.Connecting"));
6307 ui->broadcastButton->setChecked(false);
6308
6309 if (sysTrayStream) {
6310 sysTrayStream->setEnabled(false);
6311 sysTrayStream->setText(ui->streamButton->text());
6312 }
6313
6314 if (!outputHandler->StartStreaming(service)) {
6315 DisplayStreamStartError();
6316 return;
6317 }
6318
6319 if (!autoStartBroadcast) {
6320 ui->broadcastButton->setText(
6321 QTStr("Basic.Main.StartBroadcast"));
6322 ui->broadcastButton->setProperty("broadcastState", "ready");
6323 ui->broadcastButton->style()->unpolish(ui->broadcastButton);
6324 ui->broadcastButton->style()->polish(ui->broadcastButton);
6325 // well, we need to disable button while stream is not active
6326 ui->broadcastButton->setEnabled(false);
6327 } else if (!autoStopBroadcast) {
6328 broadcastActive = true;
6329 ui->broadcastButton->setText(QTStr("Basic.Main.StopBroadcast"));
6330 ui->broadcastButton->setProperty("broadcastState", "active");
6331 ui->broadcastButton->style()->unpolish(ui->broadcastButton);
6332 ui->broadcastButton->style()->polish(ui->broadcastButton);
6333 } else {
6334 ui->broadcastButton->setEnabled(false);
6335 }
6336
6337 bool recordWhenStreaming = config_get_bool(
6338 GetGlobalConfig(), "BasicWindow", "RecordWhenStreaming");
6339 if (recordWhenStreaming)
6340 StartRecording();
6341
6342 bool replayBufferWhileStreaming = config_get_bool(
6343 GetGlobalConfig(), "BasicWindow", "ReplayBufferWhileStreaming");
6344 if (replayBufferWhileStreaming)
6345 StartReplayBuffer();
6346
6347 #if YOUTUBE_ENABLED
6348 if (!autoStartBroadcast)
6349 OBSBasic::ShowYouTubeAutoStartWarning();
6350 #endif
6351 }
6352
6353 void OBSBasic::BroadcastButtonClicked()
6354 {
6355 if (!broadcastReady ||
6356 !broadcastActive && !outputHandler->StreamingActive()) {
6357 SetupBroadcast();
6358 if (broadcastReady)
6359 ui->broadcastButton->setChecked(true);
6360 return;
6361 }
6362
6363 if (!autoStartBroadcast) {
6364 #if YOUTUBE_ENABLED
6365 std::shared_ptr<YoutubeApiWrappers> ytAuth =
6366 dynamic_pointer_cast<YoutubeApiWrappers>(auth);
6367 if (ytAuth.get()) {
6368 ytAuth->StartLatestBroadcast();
6369 }
6370 #endif
6371 broadcastActive = true;
6372 autoStartBroadcast = true; // and clear the flag
6373
6374 if (!autoStopBroadcast) {
6375 ui->broadcastButton->setText(
6376 QTStr("Basic.Main.StopBroadcast"));
6377 } else {
6378 ui->broadcastButton->setText(
6379 QTStr("Basic.Main.AutoStopEnabled"));
6380 ui->broadcastButton->setEnabled(false);
6381 }
6382
6383 ui->broadcastButton->setProperty("broadcastState", "active");
6384 ui->broadcastButton->style()->unpolish(ui->broadcastButton);
6385 ui->broadcastButton->style()->polish(ui->broadcastButton);
6386 } else if (!autoStopBroadcast) {
6387 #if YOUTUBE_ENABLED
6388 bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
6389 "WarnBeforeStoppingStream");
6390 if (confirm && isVisible()) {
6391 QMessageBox::StandardButton button = OBSMessageBox::question(
6392 this, QTStr("ConfirmStop.Title"),
6393 QTStr("YouTube.Actions.AutoStopStreamingWarning"),
6394 QMessageBox::Yes | QMessageBox::No,
6395 QMessageBox::No);
6396
6397 if (button == QMessageBox::No) {
6398 ui->broadcastButton->setChecked(true);
6399 return;
6400 }
6401 }
6402
6403 std::shared_ptr<YoutubeApiWrappers> ytAuth =
6404 dynamic_pointer_cast<YoutubeApiWrappers>(auth);
6405 if (ytAuth.get()) {
6406 ytAuth->StopLatestBroadcast();
6407 }
6408 #endif
6409 broadcastActive = false;
6410 broadcastReady = false;
6411
6412 autoStopBroadcast = true;
6413 QMetaObject::invokeMethod(this, "StopStreaming");
6414 SetBroadcastFlowEnabled(true);
6415 }
6416 }
6417
6418 void OBSBasic::SetBroadcastFlowEnabled(bool enabled)
6419 {
6420 ui->broadcastButton->setEnabled(enabled);
6421 ui->broadcastButton->setVisible(enabled);
6422 ui->broadcastButton->setChecked(broadcastReady);
6423 ui->broadcastButton->setProperty("broadcastState", "idle");
6424 ui->broadcastButton->style()->unpolish(ui->broadcastButton);
6425 ui->broadcastButton->style()->polish(ui->broadcastButton);
6426 ui->broadcastButton->setText(QTStr("Basic.Main.SetupBroadcast"));
6427 }
6428
6429 void OBSBasic::SetupBroadcast()
6430 {
6431 Auth *auth = GetAuth();
6432 #if YOUTUBE_ENABLED
6433 if (IsYouTubeService(auth->service())) {
6434 OBSYoutubeActions *dialog;
6435 dialog = new OBSYoutubeActions(this, auth, broadcastReady);
6436 connect(dialog, &OBSYoutubeActions::ok, this,
6437 &OBSBasic::YouTubeActionDialogOk);
6438 int result = dialog->Valid() ? dialog->exec()
6439 : QDialog::Rejected;
6440 if (result != QDialog::Accepted) {
6441 if (!broadcastReady)
6442 ui->broadcastButton->setChecked(false);
6443 }
6444 }
6445 #endif
6446 }
6447
6448 #ifdef _WIN32
6449 static inline void UpdateProcessPriority()
6450 {
6451 const char *priority = config_get_string(App()->GlobalConfig(),
6452 "General", "ProcessPriority");
6453 if (priority && strcmp(priority, "Normal") != 0)
6454 SetProcessPriority(priority);
6455 }
6456
6457 static inline void ClearProcessPriority()
6458 {
6459 const char *priority = config_get_string(App()->GlobalConfig(),
6460 "General", "ProcessPriority");
6461 if (priority && strcmp(priority, "Normal") != 0)
6462 SetProcessPriority("Normal");
6463 }
6464 #else
6465 #define UpdateProcessPriority() \
6466 do { \
6467 } while (false)
6468 #define ClearProcessPriority() \
6469 do { \
6470 } while (false)
6471 #endif
6472
6473 inline void OBSBasic::OnActivate()
6474 {
6475 if (ui->profileMenu->isEnabled()) {
6476 ui->profileMenu->setEnabled(false);
6477 ui->autoConfigure->setEnabled(false);
6478 App()->IncrementSleepInhibition();
6479 UpdateProcessPriority();
6480
6481 if (trayIcon && trayIcon->isVisible()) {
6482 #ifdef __APPLE__
6483 QIcon trayMask =
6484 QIcon(":/res/images/tray_active_macos.png");
6485 trayMask.setIsMask(true);
6486 trayIcon->setIcon(
6487 QIcon::fromTheme("obs-tray", trayMask));
6488 #else
6489 trayIcon->setIcon(QIcon::fromTheme(
6490 "obs-tray-active",
6491 QIcon(":/res/images/tray_active.png")));
6492 #endif
6493 }
6494 }
6495 }
6496
6497 extern volatile bool recording_paused;
6498 extern volatile bool replaybuf_active;
6499
6500 inline void OBSBasic::OnDeactivate()
6501 {
6502 if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) {
6503 ui->profileMenu->setEnabled(true);
6504 ui->autoConfigure->setEnabled(true);
6505 App()->DecrementSleepInhibition();
6506 ClearProcessPriority();
6507
6508 if (trayIcon && trayIcon->isVisible()) {
6509 #ifdef __APPLE__
6510 QIcon trayIconFile =
6511 QIcon(":/res/images/obs_macos.png");
6512 trayIconFile.setIsMask(true);
6513 #else
6514 QIcon trayIconFile = QIcon(":/res/images/obs.png");
6515 #endif
6516 trayIcon->setIcon(
6517 QIcon::fromTheme("obs-tray", trayIconFile));
6518 }
6519 } else if (outputHandler->Active() && trayIcon &&
6520 trayIcon->isVisible()) {
6521 if (os_atomic_load_bool(&recording_paused)) {
6522 #ifdef __APPLE__
6523 QIcon trayIconFile =
6524 QIcon(":/res/images/obs_paused_macos.png");
6525 trayIconFile.setIsMask(true);
6526 #else
6527 QIcon trayIconFile =
6528 QIcon(":/res/images/obs_paused.png");
6529 #endif
6530 trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused",
6531 trayIconFile));
6532 } else {
6533 #ifdef __APPLE__
6534 QIcon trayIconFile =
6535 QIcon(":/res/images/tray_active_macos.png");
6536 trayIconFile.setIsMask(true);
6537 #else
6538 QIcon trayIconFile =
6539 QIcon(":/res/images/tray_active.png");
6540 #endif
6541 trayIcon->setIcon(QIcon::fromTheme("obs-tray-active",
6542 trayIconFile));
6543 }
6544 }
6545 }
6546
6547 void OBSBasic::StopStreaming()
6548 {
6549 SaveProject();
6550
6551 if (outputHandler->StreamingActive())
6552 outputHandler->StopStreaming(streamingStopping);
6553
6554 // special case: force reset broadcast state if
6555 // no autostart and no autostop selected
6556 if (!autoStartBroadcast && !broadcastActive) {
6557 broadcastActive = false;
6558 autoStartBroadcast = true;
6559 autoStopBroadcast = true;
6560 broadcastReady = false;
6561 }
6562
6563 if (autoStopBroadcast) {
6564 broadcastActive = false;
6565 broadcastReady = false;
6566 }
6567
6568 OnDeactivate();
6569
6570 bool recordWhenStreaming = config_get_bool(
6571 GetGlobalConfig(), "BasicWindow", "RecordWhenStreaming");
6572 bool keepRecordingWhenStreamStops =
6573 config_get_bool(GetGlobalConfig(), "BasicWindow",
6574 "KeepRecordingWhenStreamStops");
6575 if (recordWhenStreaming && !keepRecordingWhenStreamStops)
6576 StopRecording();
6577
6578 bool replayBufferWhileStreaming = config_get_bool(
6579 GetGlobalConfig(), "BasicWindow", "ReplayBufferWhileStreaming");
6580 bool keepReplayBufferStreamStops =
6581 config_get_bool(GetGlobalConfig(), "BasicWindow",
6582 "KeepReplayBufferStreamStops");
6583 if (replayBufferWhileStreaming && !keepReplayBufferStreamStops)
6584 StopReplayBuffer();
6585 }
6586
6587 void OBSBasic::ForceStopStreaming()
6588 {
6589 SaveProject();
6590
6591 if (outputHandler->StreamingActive())
6592 outputHandler->StopStreaming(true);
6593
6594 // special case: force reset broadcast state if
6595 // no autostart and no autostop selected
6596 if (!autoStartBroadcast && !broadcastActive) {
6597 broadcastActive = false;
6598 autoStartBroadcast = true;
6599 autoStopBroadcast = true;
6600 broadcastReady = false;
6601 }
6602
6603 if (autoStopBroadcast) {
6604 broadcastActive = false;
6605 broadcastReady = false;
6606 }
6607
6608 OnDeactivate();
6609
6610 bool recordWhenStreaming = config_get_bool(
6611 GetGlobalConfig(), "BasicWindow", "RecordWhenStreaming");
6612 bool keepRecordingWhenStreamStops =
6613 config_get_bool(GetGlobalConfig(), "BasicWindow",
6614 "KeepRecordingWhenStreamStops");
6615 if (recordWhenStreaming && !keepRecordingWhenStreamStops)
6616 StopRecording();
6617
6618 bool replayBufferWhileStreaming = config_get_bool(
6619 GetGlobalConfig(), "BasicWindow", "ReplayBufferWhileStreaming");
6620 bool keepReplayBufferStreamStops =
6621 config_get_bool(GetGlobalConfig(), "BasicWindow",
6622 "KeepReplayBufferStreamStops");
6623 if (replayBufferWhileStreaming && !keepReplayBufferStreamStops)
6624 StopReplayBuffer();
6625 }
6626
6627 void OBSBasic::StreamDelayStarting(int sec)
6628 {
6629 ui->streamButton->setText(QTStr("Basic.Main.StopStreaming"));
6630 ui->streamButton->setEnabled(true);
6631 ui->streamButton->setChecked(true);
6632
6633 if (sysTrayStream) {
6634 sysTrayStream->setText(ui->streamButton->text());
6635 sysTrayStream->setEnabled(true);
6636 }
6637
6638 if (!startStreamMenu.isNull())
6639 startStreamMenu->deleteLater();
6640
6641 startStreamMenu = new QMenu();
6642 startStreamMenu->addAction(QTStr("Basic.Main.StopStreaming"), this,
6643 SLOT(StopStreaming()));
6644 startStreamMenu->addAction(QTStr("Basic.Main.ForceStopStreaming"), this,
6645 SLOT(ForceStopStreaming()));
6646 ui->streamButton->setMenu(startStreamMenu);
6647
6648 ui->statusbar->StreamDelayStarting(sec);
6649
6650 OnActivate();
6651 }
6652
6653 void OBSBasic::StreamDelayStopping(int sec)
6654 {
6655 ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
6656 ui->streamButton->setEnabled(true);
6657 ui->streamButton->setChecked(false);
6658
6659 if (sysTrayStream) {
6660 sysTrayStream->setText(ui->streamButton->text());
6661 sysTrayStream->setEnabled(true);
6662 }
6663
6664 if (!startStreamMenu.isNull())
6665 startStreamMenu->deleteLater();
6666
6667 startStreamMenu = new QMenu();
6668 startStreamMenu->addAction(QTStr("Basic.Main.StartStreaming"), this,
6669 SLOT(StartStreaming()));
6670 startStreamMenu->addAction(QTStr("Basic.Main.ForceStopStreaming"), this,
6671 SLOT(ForceStopStreaming()));
6672 ui->streamButton->setMenu(startStreamMenu);
6673
6674 ui->statusbar->StreamDelayStopping(sec);
6675
6676 if (api)
6677 api->on_event(OBS_FRONTEND_EVENT_STREAMING_STOPPING);
6678 }
6679
6680 void OBSBasic::StreamingStart()
6681 {
6682 ui->streamButton->setText(QTStr("Basic.Main.StopStreaming"));
6683 ui->streamButton->setEnabled(true);
6684 ui->streamButton->setChecked(true);
6685 ui->statusbar->StreamStarted(outputHandler->streamOutput);
6686
6687 if (sysTrayStream) {
6688 sysTrayStream->setText(ui->streamButton->text());
6689 sysTrayStream->setEnabled(true);
6690 }
6691
6692 #if YOUTUBE_ENABLED
6693 if (!autoStartBroadcast) {
6694 // get a current stream key
6695 obs_service_t *service_obj = GetService();
6696 obs_data_t *settings = obs_service_get_settings(service_obj);
6697 std::string key = obs_data_get_string(settings, "stream_id");
6698 if (!key.empty() && !youtubeStreamCheckThread) {
6699 youtubeStreamCheckThread = CreateQThread(
6700 [this, key] { YoutubeStreamCheck(key); });
6701 youtubeStreamCheckThread->setObjectName(
6702 "YouTubeStreamCheckThread");
6703 youtubeStreamCheckThread->start();
6704 }
6705 }
6706 #endif
6707
6708 if (api)
6709 api->on_event(OBS_FRONTEND_EVENT_STREAMING_STARTED);
6710
6711 OnActivate();
6712
6713 blog(LOG_INFO, STREAMING_START);
6714 }
6715
6716 void OBSBasic::StreamStopping()
6717 {
6718 ui->streamButton->setText(QTStr("Basic.Main.StoppingStreaming"));
6719
6720 if (sysTrayStream)
6721 sysTrayStream->setText(ui->streamButton->text());
6722
6723 streamingStopping = true;
6724 if (api)
6725 api->on_event(OBS_FRONTEND_EVENT_STREAMING_STOPPING);
6726 }
6727
6728 void OBSBasic::StreamingStop(int code, QString last_error)
6729 {
6730 const char *errorDescription = "";
6731 DStr errorMessage;
6732 bool use_last_error = false;
6733 bool encode_error = false;
6734
6735 switch (code) {
6736 case OBS_OUTPUT_BAD_PATH:
6737 errorDescription = Str("Output.ConnectFail.BadPath");
6738 break;
6739
6740 case OBS_OUTPUT_CONNECT_FAILED:
6741 use_last_error = true;
6742 errorDescription = Str("Output.ConnectFail.ConnectFailed");
6743 break;
6744
6745 case OBS_OUTPUT_INVALID_STREAM:
6746 errorDescription = Str("Output.ConnectFail.InvalidStream");
6747 break;
6748
6749 case OBS_OUTPUT_ENCODE_ERROR:
6750 encode_error = true;
6751 break;
6752
6753 default:
6754 case OBS_OUTPUT_ERROR:
6755 use_last_error = true;
6756 errorDescription = Str("Output.ConnectFail.Error");
6757 break;
6758
6759 case OBS_OUTPUT_DISCONNECTED:
6760 /* doesn't happen if output is set to reconnect. note that
6761 * reconnects are handled in the output, not in the UI */
6762 use_last_error = true;
6763 errorDescription = Str("Output.ConnectFail.Disconnected");
6764 }
6765
6766 if (use_last_error && !last_error.isEmpty())
6767 dstr_printf(errorMessage, "%s\n\n%s", errorDescription,
6768 QT_TO_UTF8(last_error));
6769 else
6770 dstr_copy(errorMessage, errorDescription);
6771
6772 ui->statusbar->StreamStopped();
6773
6774 ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
6775 ui->streamButton->setEnabled(true);
6776 ui->streamButton->setChecked(false);
6777
6778 if (sysTrayStream) {
6779 sysTrayStream->setText(ui->streamButton->text());
6780 sysTrayStream->setEnabled(true);
6781 }
6782
6783 streamingStopping = false;
6784 if (api)
6785 api->on_event(OBS_FRONTEND_EVENT_STREAMING_STOPPED);
6786
6787 OnDeactivate();
6788
6789 blog(LOG_INFO, STREAMING_STOP);
6790
6791 if (encode_error) {
6792 OBSMessageBox::information(
6793 this, QTStr("Output.StreamEncodeError.Title"),
6794 QTStr("Output.StreamEncodeError.Msg"));
6795
6796 } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) {
6797 OBSMessageBox::information(this,
6798 QTStr("Output.ConnectFail.Title"),
6799 QT_UTF8(errorMessage));
6800
6801 } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) {
6802 SysTrayNotify(QT_UTF8(errorDescription),
6803 QSystemTrayIcon::Warning);
6804 }
6805
6806 if (!startStreamMenu.isNull()) {
6807 ui->streamButton->setMenu(nullptr);
6808 startStreamMenu->deleteLater();
6809 startStreamMenu = nullptr;
6810 }
6811
6812 // Reset broadcast button state/text
6813 if (!broadcastActive)
6814 SetBroadcastFlowEnabled(auth && auth->broadcastFlow());
6815 }
6816
6817 void OBSBasic::AutoRemux(QString input)
6818 {
6819 bool autoRemux = config_get_bool(Config(), "Video", "AutoRemux");
6820
6821 if (!autoRemux)
6822 return;
6823
6824 const char *recType = config_get_string(Config(), "AdvOut", "RecType");
6825
6826 bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0;
6827
6828 if (ffmpegOutput)
6829 return;
6830
6831 if (input.isEmpty())
6832 return;
6833
6834 QFileInfo fi(input);
6835 QString suffix = fi.suffix();
6836
6837 /* do not remux if lossless */
6838 if (suffix.compare("avi", Qt::CaseInsensitive) == 0) {
6839 return;
6840 }
6841
6842 QString path = fi.path();
6843
6844 QString output = input;
6845 output.resize(output.size() - suffix.size());
6846 output += "mp4";
6847
6848 OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true);
6849 remux->show();
6850 remux->AutoRemux(input, output);
6851 }
6852
6853 void OBSBasic::StartRecording()
6854 {
6855 if (outputHandler->RecordingActive())
6856 return;
6857 if (disableOutputsRef)
6858 return;
6859
6860 if (!OutputPathValid()) {
6861 OutputPathInvalidMessage();
6862 ui->recordButton->setChecked(false);
6863 return;
6864 }
6865
6866 if (LowDiskSpace()) {
6867 DiskSpaceMessage();
6868 ui->recordButton->setChecked(false);
6869 return;
6870 }
6871
6872 if (api)
6873 api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTING);
6874
6875 SaveProject();
6876
6877 if (!outputHandler->StartRecording())
6878 ui->recordButton->setChecked(false);
6879 }
6880
6881 void OBSBasic::RecordStopping()
6882 {
6883 ui->recordButton->setText(QTStr("Basic.Main.StoppingRecording"));
6884
6885 if (sysTrayRecord)
6886 sysTrayRecord->setText(ui->recordButton->text());
6887
6888 recordingStopping = true;
6889 if (api)
6890 api->on_event(OBS_FRONTEND_EVENT_RECORDING_STOPPING);
6891 }
6892
6893 void OBSBasic::StopRecording()
6894 {
6895 SaveProject();
6896
6897 if (outputHandler->RecordingActive())
6898 outputHandler->StopRecording(recordingStopping);
6899
6900 OnDeactivate();
6901 }
6902
6903 void OBSBasic::RecordingStart()
6904 {
6905 ui->statusbar->RecordingStarted(outputHandler->fileOutput);
6906 ui->recordButton->setText(QTStr("Basic.Main.StopRecording"));
6907 ui->recordButton->setChecked(true);
6908
6909 if (sysTrayRecord)
6910 sysTrayRecord->setText(ui->recordButton->text());
6911
6912 recordingStopping = false;
6913 if (api)
6914 api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTED);
6915
6916 if (!diskFullTimer->isActive())
6917 diskFullTimer->start(1000);
6918
6919 OnActivate();
6920 UpdatePause();
6921
6922 blog(LOG_INFO, RECORDING_START);
6923 }
6924
6925 void OBSBasic::RecordingStop(int code, QString last_error)
6926 {
6927 ui->statusbar->RecordingStopped();
6928 ui->recordButton->setText(QTStr("Basic.Main.StartRecording"));
6929 ui->recordButton->setChecked(false);
6930
6931 if (sysTrayRecord)
6932 sysTrayRecord->setText(ui->recordButton->text());
6933
6934 blog(LOG_INFO, RECORDING_STOP);
6935
6936 if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) {
6937 OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"),
6938 QTStr("Output.RecordFail.Unsupported"));
6939
6940 } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) {
6941 OBSMessageBox::warning(
6942 this, QTStr("Output.RecordError.Title"),
6943 QTStr("Output.RecordError.EncodeErrorMsg"));
6944
6945 } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) {
6946 OBSMessageBox::warning(this,
6947 QTStr("Output.RecordNoSpace.Title"),
6948 QTStr("Output.RecordNoSpace.Msg"));
6949
6950 } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) {
6951
6952 const char *errorDescription;
6953 DStr errorMessage;
6954 bool use_last_error = true;
6955
6956 errorDescription = Str("Output.RecordError.Msg");
6957
6958 if (use_last_error && !last_error.isEmpty())
6959 dstr_printf(errorMessage, "%s\n\n%s", errorDescription,
6960 QT_TO_UTF8(last_error));
6961 else
6962 dstr_copy(errorMessage, errorDescription);
6963
6964 OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"),
6965 QT_UTF8(errorMessage));
6966
6967 } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) {
6968 SysTrayNotify(QTStr("Output.RecordFail.Unsupported"),
6969 QSystemTrayIcon::Warning);
6970
6971 } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) {
6972 SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"),
6973 QSystemTrayIcon::Warning);
6974
6975 } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) {
6976 SysTrayNotify(QTStr("Output.RecordError.Msg"),
6977 QSystemTrayIcon::Warning);
6978 } else if (code == OBS_OUTPUT_SUCCESS) {
6979 if (outputHandler) {
6980 std::string path = outputHandler->lastRecordingPath;
6981 QString str = QTStr("Basic.StatusBar.RecordingSavedTo");
6982 ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str())));
6983 }
6984 }
6985
6986 if (api)
6987 api->on_event(OBS_FRONTEND_EVENT_RECORDING_STOPPED);
6988
6989 if (diskFullTimer->isActive())
6990 diskFullTimer->stop();
6991
6992 AutoRemux(outputHandler->lastRecordingPath.c_str());
6993
6994 OnDeactivate();
6995 UpdatePause(false);
6996 }
6997
6998 void OBSBasic::ShowReplayBufferPauseWarning()
6999 {
7000 auto msgBox = []() {
7001 QMessageBox msgbox(App()->GetMainWindow());
7002 msgbox.setWindowTitle(QTStr("Output.ReplayBuffer."
7003 "PauseWarning.Title"));
7004 msgbox.setText(QTStr("Output.ReplayBuffer."
7005 "PauseWarning.Text"));
7006 msgbox.setIcon(QMessageBox::Icon::Information);
7007 msgbox.addButton(QMessageBox::Ok);
7008
7009 QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain"));
7010 msgbox.setCheckBox(cb);
7011
7012 msgbox.exec();
7013
7014 if (cb->isChecked()) {
7015 config_set_bool(App()->GlobalConfig(), "General",
7016 "WarnedAboutReplayBufferPausing", true);
7017 config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
7018 }
7019 };
7020
7021 bool warned = config_get_bool(App()->GlobalConfig(), "General",
7022 "WarnedAboutReplayBufferPausing");
7023 if (!warned) {
7024 QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection,
7025 Q_ARG(VoidFunc, msgBox));
7026 }
7027 }
7028
7029 void OBSBasic::StartReplayBuffer()
7030 {
7031 if (!outputHandler || !outputHandler->replayBuffer)
7032 return;
7033 if (outputHandler->ReplayBufferActive())
7034 return;
7035 if (disableOutputsRef)
7036 return;
7037
7038 if (!UIValidation::NoSourcesConfirmation(this)) {
7039 replayBufferButton->setChecked(false);
7040 return;
7041 }
7042
7043 if (LowDiskSpace()) {
7044 DiskSpaceMessage();
7045 replayBufferButton->setChecked(false);
7046 return;
7047 }
7048
7049 if (api)
7050 api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING);
7051
7052 SaveProject();
7053
7054 if (!outputHandler->StartReplayBuffer()) {
7055 replayBufferButton->setChecked(false);
7056 } else if (os_atomic_load_bool(&recording_paused)) {
7057 ShowReplayBufferPauseWarning();
7058 }
7059 }
7060
7061 void OBSBasic::ReplayBufferStopping()
7062 {
7063 if (!outputHandler || !outputHandler->replayBuffer)
7064 return;
7065
7066 replayBufferButton->setText(QTStr("Basic.Main.StoppingReplayBuffer"));
7067
7068 if (sysTrayReplayBuffer)
7069 sysTrayReplayBuffer->setText(replayBufferButton->text());
7070
7071 replayBufferStopping = true;
7072 if (api)
7073 api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING);
7074 }
7075
7076 void OBSBasic::StopReplayBuffer()
7077 {
7078 if (!outputHandler || !outputHandler->replayBuffer)
7079 return;
7080
7081 SaveProject();
7082
7083 if (outputHandler->ReplayBufferActive())
7084 outputHandler->StopReplayBuffer(replayBufferStopping);
7085
7086 OnDeactivate();
7087 }
7088
7089 void OBSBasic::ReplayBufferStart()
7090 {
7091 if (!outputHandler || !outputHandler->replayBuffer)
7092 return;
7093
7094 replayBufferButton->setText(QTStr("Basic.Main.StopReplayBuffer"));
7095 replayBufferButton->setChecked(true);
7096
7097 if (sysTrayReplayBuffer)
7098 sysTrayReplayBuffer->setText(replayBufferButton->text());
7099
7100 replayBufferStopping = false;
7101 if (api)
7102 api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED);
7103
7104 OnActivate();
7105 UpdateReplayBuffer();
7106
7107 blog(LOG_INFO, REPLAY_BUFFER_START);
7108 }
7109
7110 void OBSBasic::ReplayBufferSave()
7111 {
7112 if (!outputHandler || !outputHandler->replayBuffer)
7113 return;
7114 if (!outputHandler->ReplayBufferActive())
7115 return;
7116
7117 calldata_t cd = {0};
7118 proc_handler_t *ph =
7119 obs_output_get_proc_handler(outputHandler->replayBuffer);
7120 proc_handler_call(ph, "save", &cd);
7121 calldata_free(&cd);
7122 }
7123
7124 void OBSBasic::ReplayBufferSaved()
7125 {
7126 if (!outputHandler || !outputHandler->replayBuffer)
7127 return;
7128 if (!outputHandler->ReplayBufferActive())
7129 return;
7130
7131 calldata_t cd = {0};
7132 proc_handler_t *ph =
7133 obs_output_get_proc_handler(outputHandler->replayBuffer);
7134 proc_handler_call(ph, "get_last_replay", &cd);
7135 QString path = QT_UTF8(calldata_string(&cd, "path"));
7136 QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(path);
7137 ShowStatusBarMessage(msg);
7138 calldata_free(&cd);
7139
7140 if (api)
7141 api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED);
7142
7143 AutoRemux(path);
7144 }
7145
7146 void OBSBasic::ReplayBufferStop(int code)
7147 {
7148 if (!outputHandler || !outputHandler->replayBuffer)
7149 return;
7150
7151 replayBufferButton->setText(QTStr("Basic.Main.StartReplayBuffer"));
7152 replayBufferButton->setChecked(false);
7153
7154 if (sysTrayReplayBuffer)
7155 sysTrayReplayBuffer->setText(replayBufferButton->text());
7156
7157 blog(LOG_INFO, REPLAY_BUFFER_STOP);
7158
7159 if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) {
7160 OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"),
7161 QTStr("Output.RecordFail.Unsupported"));
7162
7163 } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) {
7164 OBSMessageBox::warning(this,
7165 QTStr("Output.RecordNoSpace.Title"),
7166 QTStr("Output.RecordNoSpace.Msg"));
7167
7168 } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) {
7169 OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"),
7170 QTStr("Output.RecordError.Msg"));
7171
7172 } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) {
7173 SysTrayNotify(QTStr("Output.RecordFail.Unsupported"),
7174 QSystemTrayIcon::Warning);
7175
7176 } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) {
7177 SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"),
7178 QSystemTrayIcon::Warning);
7179
7180 } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) {
7181 SysTrayNotify(QTStr("Output.RecordError.Msg"),
7182 QSystemTrayIcon::Warning);
7183 }
7184
7185 if (api)
7186 api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED);
7187
7188 OnDeactivate();
7189 UpdateReplayBuffer(false);
7190 }
7191
7192 void OBSBasic::StartVirtualCam()
7193 {
7194 if (!outputHandler || !outputHandler->virtualCam)
7195 return;
7196 if (outputHandler->VirtualCamActive())
7197 return;
7198 if (disableOutputsRef)
7199 return;
7200
7201 SaveProject();
7202
7203 if (!outputHandler->StartVirtualCam()) {
7204 vcamButton->setChecked(false);
7205 }
7206 }
7207
7208 void OBSBasic::StopVirtualCam()
7209 {
7210 if (!outputHandler || !outputHandler->virtualCam)
7211 return;
7212
7213 SaveProject();
7214
7215 if (outputHandler->VirtualCamActive())
7216 outputHandler->StopVirtualCam();
7217
7218 OnDeactivate();
7219 }
7220
7221 void OBSBasic::OnVirtualCamStart()
7222 {
7223 if (!outputHandler || !outputHandler->virtualCam)
7224 return;
7225
7226 vcamButton->setText(QTStr("Basic.Main.StopVirtualCam"));
7227 if (sysTrayVirtualCam)
7228 sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam"));
7229 vcamButton->setChecked(true);
7230
7231 if (api)
7232 api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED);
7233
7234 OnActivate();
7235
7236 blog(LOG_INFO, VIRTUAL_CAM_START);
7237 }
7238
7239 void OBSBasic::OnVirtualCamStop(int)
7240 {
7241 if (!outputHandler || !outputHandler->virtualCam)
7242 return;
7243
7244 vcamButton->setText(QTStr("Basic.Main.StartVirtualCam"));
7245 if (sysTrayVirtualCam)
7246 sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam"));
7247 vcamButton->setChecked(false);
7248
7249 if (api)
7250 api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED);
7251
7252 blog(LOG_INFO, VIRTUAL_CAM_STOP);
7253
7254 OnDeactivate();
7255 }
7256
7257 void OBSBasic::on_streamButton_clicked()
7258 {
7259 if (outputHandler->StreamingActive()) {
7260 bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
7261 "WarnBeforeStoppingStream");
7262
7263 #if YOUTUBE_ENABLED
7264 if (isVisible() && auth && IsYouTubeService(auth->service()) &&
7265 autoStopBroadcast) {
7266 QMessageBox::StandardButton button = OBSMessageBox::question(
7267 this, QTStr("ConfirmStop.Title"),
7268 QTStr("YouTube.Actions.AutoStopStreamingWarning"),
7269 QMessageBox::Yes | QMessageBox::No,
7270 QMessageBox::No);
7271
7272 if (button == QMessageBox::No) {
7273 ui->streamButton->setChecked(true);
7274 return;
7275 }
7276
7277 confirm = false;
7278 }
7279 #endif
7280 if (confirm && isVisible()) {
7281 QMessageBox::StandardButton button =
7282 OBSMessageBox::question(
7283 this, QTStr("ConfirmStop.Title"),
7284 QTStr("ConfirmStop.Text"),
7285 QMessageBox::Yes | QMessageBox::No,
7286 QMessageBox::No);
7287
7288 if (button == QMessageBox::No) {
7289 ui->streamButton->setChecked(true);
7290 return;
7291 }
7292 }
7293
7294 StopStreaming();
7295 } else {
7296 if (!UIValidation::NoSourcesConfirmation(this)) {
7297 ui->streamButton->setChecked(false);
7298 return;
7299 }
7300
7301 Auth *auth = GetAuth();
7302
7303 auto action =
7304 (auth && auth->external())
7305 ? StreamSettingsAction::ContinueStream
7306 : UIValidation::StreamSettingsConfirmation(
7307 this, service);
7308 switch (action) {
7309 case StreamSettingsAction::ContinueStream:
7310 break;
7311 case StreamSettingsAction::OpenSettings:
7312 on_action_Settings_triggered();
7313 ui->streamButton->setChecked(false);
7314 return;
7315 case StreamSettingsAction::Cancel:
7316 ui->streamButton->setChecked(false);
7317 return;
7318 }
7319
7320 bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
7321 "WarnBeforeStartingStream");
7322
7323 bool bwtest = false;
7324
7325 if (this->auth) {
7326 obs_data_t *settings =
7327 obs_service_get_settings(service);
7328 bwtest = obs_data_get_bool(settings, "bwtest");
7329 obs_data_release(settings);
7330 // Disable confirmation if this is going to open broadcast setup
7331 if (auth && auth->broadcastFlow() && !broadcastReady &&
7332 !broadcastActive)
7333 confirm = false;
7334 }
7335
7336 if (bwtest && isVisible()) {
7337 QMessageBox::StandardButton button =
7338 OBSMessageBox::question(
7339 this, QTStr("ConfirmBWTest.Title"),
7340 QTStr("ConfirmBWTest.Text"));
7341
7342 if (button == QMessageBox::No) {
7343 ui->streamButton->setChecked(false);
7344 return;
7345 }
7346 } else if (confirm && isVisible()) {
7347 QMessageBox::StandardButton button =
7348 OBSMessageBox::question(
7349 this, QTStr("ConfirmStart.Title"),
7350 QTStr("ConfirmStart.Text"),
7351 QMessageBox::Yes | QMessageBox::No,
7352 QMessageBox::No);
7353
7354 if (button == QMessageBox::No) {
7355 ui->streamButton->setChecked(false);
7356 return;
7357 }
7358 }
7359
7360 StartStreaming();
7361 }
7362 }
7363
7364 void OBSBasic::on_recordButton_clicked()
7365 {
7366 if (outputHandler->RecordingActive()) {
7367 bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
7368 "WarnBeforeStoppingRecord");
7369
7370 if (confirm && isVisible()) {
7371 QMessageBox::StandardButton button =
7372 OBSMessageBox::question(
7373 this, QTStr("ConfirmStopRecord.Title"),
7374 QTStr("ConfirmStopRecord.Text"),
7375 QMessageBox::Yes | QMessageBox::No,
7376 QMessageBox::No);
7377
7378 if (button == QMessageBox::No) {
7379 ui->recordButton->setChecked(true);
7380 return;
7381 }
7382 }
7383 StopRecording();
7384 } else {
7385 if (!UIValidation::NoSourcesConfirmation(this)) {
7386 ui->recordButton->setChecked(false);
7387 return;
7388 }
7389
7390 StartRecording();
7391 }
7392 }
7393
7394 void OBSBasic::VCamButtonClicked()
7395 {
7396 if (outputHandler->VirtualCamActive()) {
7397 StopVirtualCam();
7398 } else {
7399 if (!UIValidation::NoSourcesConfirmation(this)) {
7400 vcamButton->setChecked(false);
7401 return;
7402 }
7403
7404 StartVirtualCam();
7405 }
7406 }
7407
7408 void OBSBasic::on_settingsButton_clicked()
7409 {
7410 on_action_Settings_triggered();
7411 }
7412
7413 void OBSBasic::on_actionHelpPortal_triggered()
7414 {
7415 QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode);
7416 QDesktopServices::openUrl(url);
7417 }
7418
7419 void OBSBasic::on_actionWebsite_triggered()
7420 {
7421 QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode);
7422 QDesktopServices::openUrl(url);
7423 }
7424
7425 void OBSBasic::on_actionDiscord_triggered()
7426 {
7427 QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode);
7428 QDesktopServices::openUrl(url);
7429 }
7430
7431 void OBSBasic::on_actionShowSettingsFolder_triggered()
7432 {
7433 char path[512];
7434 int ret = GetConfigPath(path, 512, "obs-studio");
7435 if (ret <= 0)
7436 return;
7437
7438 QDesktopServices::openUrl(QUrl::fromLocalFile(path));
7439 }
7440
7441 void OBSBasic::on_actionShowProfileFolder_triggered()
7442 {
7443 char path[512];
7444 int ret = GetProfilePath(path, 512, "");
7445 if (ret <= 0)
7446 return;
7447
7448 QDesktopServices::openUrl(QUrl::fromLocalFile(path));
7449 }
7450
7451 int OBSBasic::GetTopSelectedSourceItem()
7452 {
7453 QModelIndexList selectedItems =
7454 ui->sources->selectionModel()->selectedIndexes();
7455 return selectedItems.count() ? selectedItems[0].row() : -1;
7456 }
7457
7458 QModelIndexList OBSBasic::GetAllSelectedSourceItems()
7459 {
7460 return ui->sources->selectionModel()->selectedIndexes();
7461 }
7462
7463 void OBSBasic::on_preview_customContextMenuRequested(const QPoint &pos)
7464 {
7465 CreateSourcePopupMenu(GetTopSelectedSourceItem(), true);
7466
7467 UNUSED_PARAMETER(pos);
7468 }
7469
7470 void OBSBasic::ProgramViewContextMenuRequested(const QPoint &)
7471 {
7472 QMenu popup(this);
7473 QPointer<QMenu> studioProgramProjector;
7474
7475 studioProgramProjector = new QMenu(QTStr("StudioProgramProjector"));
7476 AddProjectorMenuMonitors(studioProgramProjector, this,
7477 SLOT(OpenStudioProgramProjector()));
7478
7479 popup.addMenu(studioProgramProjector);
7480
7481 QAction *studioProgramWindow =
7482 popup.addAction(QTStr("StudioProgramWindow"), this,
7483 SLOT(OpenStudioProgramWindow()));
7484
7485 popup.addAction(studioProgramWindow);
7486
7487 popup.addAction(QTStr("Screenshot.StudioProgram"), this,
7488 SLOT(ScreenshotProgram()));
7489
7490 popup.exec(QCursor::pos());
7491 }
7492
7493 void OBSBasic::PreviewDisabledMenu(const QPoint &pos)
7494 {
7495 QMenu popup(this);
7496 delete previewProjectorMain;
7497
7498 QAction *action =
7499 popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"),
7500 this, SLOT(TogglePreview()));
7501 action->setCheckable(true);
7502 action->setChecked(obs_display_enabled(ui->preview->GetDisplay()));
7503
7504 previewProjectorMain = new QMenu(QTStr("PreviewProjector"));
7505 AddProjectorMenuMonitors(previewProjectorMain, this,
7506 SLOT(OpenPreviewProjector()));
7507
7508 QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this,
7509 SLOT(OpenPreviewWindow()));
7510
7511 popup.addMenu(previewProjectorMain);
7512 popup.addAction(previewWindow);
7513 popup.exec(QCursor::pos());
7514
7515 UNUSED_PARAMETER(pos);
7516 }
7517
7518 void OBSBasic::on_actionAlwaysOnTop_triggered()
7519 {
7520 #ifndef _WIN32
7521 /* Make sure all dialogs are safely and successfully closed before
7522 * switching the always on top mode due to the fact that windows all
7523 * have to be recreated, so queue the actual toggle to happen after
7524 * all events related to closing the dialogs have finished */
7525 CloseDialogs();
7526 #endif
7527
7528 QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop",
7529 Qt::QueuedConnection);
7530 }
7531
7532 void OBSBasic::ToggleAlwaysOnTop()
7533 {
7534 bool isAlwaysOnTop = IsAlwaysOnTop(this);
7535
7536 ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop);
7537 SetAlwaysOnTop(this, !isAlwaysOnTop);
7538
7539 show();
7540 }
7541
7542 void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const
7543 {
7544 const char *val = config_get_string(basicConfig, "Video", "FPSCommon");
7545
7546 if (strcmp(val, "10") == 0) {
7547 num = 10;
7548 den = 1;
7549 } else if (strcmp(val, "20") == 0) {
7550 num = 20;
7551 den = 1;
7552 } else if (strcmp(val, "24 NTSC") == 0) {
7553 num = 24000;
7554 den = 1001;
7555 } else if (strcmp(val, "25 PAL") == 0) {
7556 num = 25;
7557 den = 1;
7558 } else if (strcmp(val, "29.97") == 0) {
7559 num = 30000;
7560 den = 1001;
7561 } else if (strcmp(val, "48") == 0) {
7562 num = 48;
7563 den = 1;
7564 } else if (strcmp(val, "50 PAL") == 0) {
7565 num = 50;
7566 den = 1;
7567 } else if (strcmp(val, "59.94") == 0) {
7568 num = 60000;
7569 den = 1001;
7570 } else if (strcmp(val, "60") == 0) {
7571 num = 60;
7572 den = 1;
7573 } else {
7574 num = 30;
7575 den = 1;
7576 }
7577 }
7578
7579 void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const
7580 {
7581 num = (uint32_t)config_get_uint(basicConfig, "Video", "FPSInt");
7582 den = 1;
7583 }
7584
7585 void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const
7586 {
7587 num = (uint32_t)config_get_uint(basicConfig, "Video", "FPSNum");
7588 den = (uint32_t)config_get_uint(basicConfig, "Video", "FPSDen");
7589 }
7590
7591 void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const
7592 {
7593 num = 1000000000;
7594 den = (uint32_t)config_get_uint(basicConfig, "Video", "FPSNS");
7595 }
7596
7597 void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const
7598 {
7599 uint32_t type = config_get_uint(basicConfig, "Video", "FPSType");
7600
7601 if (type == 1) //"Integer"
7602 GetFPSInteger(num, den);
7603 else if (type == 2) //"Fraction"
7604 GetFPSFraction(num, den);
7605 else if (false) //"Nanoseconds", currently not implemented
7606 GetFPSNanoseconds(num, den);
7607 else
7608 GetFPSCommon(num, den);
7609 }
7610
7611 config_t *OBSBasic::Config() const
7612 {
7613 return basicConfig;
7614 }
7615
7616 void OBSBasic::on_actionEditTransform_triggered()
7617 {
7618 if (transformWindow)
7619 transformWindow->close();
7620
7621 if (!GetCurrentSceneItem())
7622 return;
7623
7624 transformWindow = new OBSBasicTransform(this);
7625 transformWindow->show();
7626 transformWindow->setAttribute(Qt::WA_DeleteOnClose, true);
7627 }
7628
7629 static obs_transform_info copiedTransformInfo;
7630 static obs_sceneitem_crop copiedCropInfo;
7631
7632 void OBSBasic::on_actionCopyTransform_triggered()
7633 {
7634 auto func = [](obs_scene_t *scene, obs_sceneitem_t *item, void *param) {
7635 if (!obs_sceneitem_selected(item))
7636 return true;
7637
7638 obs_sceneitem_defer_update_begin(item);
7639 obs_sceneitem_get_info(item, &copiedTransformInfo);
7640 obs_sceneitem_get_crop(item, &copiedCropInfo);
7641 obs_sceneitem_defer_update_end(item);
7642
7643 UNUSED_PARAMETER(scene);
7644 UNUSED_PARAMETER(param);
7645 return true;
7646 };
7647
7648 obs_scene_enum_items(GetCurrentScene(), func, nullptr);
7649 ui->actionPasteTransform->setEnabled(true);
7650 }
7651
7652 void undo_redo(const std::string &data)
7653 {
7654 obs_data_t *dat = obs_data_create_from_json(data.c_str());
7655 obs_source_t *source =
7656 obs_get_source_by_name(obs_data_get_string(dat, "scene_name"));
7657 reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
7658 ->SetCurrentScene(source, true);
7659 obs_source_release(source);
7660 obs_data_release(dat);
7661
7662 obs_scene_load_transform_states(data.c_str());
7663 }
7664
7665 void OBSBasic::on_actionPasteTransform_triggered()
7666 {
7667 obs_data_t *wrapper =
7668 obs_scene_save_transform_states(GetCurrentScene(), false);
7669 auto func = [](obs_scene_t *scene, obs_sceneitem_t *item, void *param) {
7670 if (!obs_sceneitem_selected(item))
7671 return true;
7672
7673 obs_sceneitem_defer_update_begin(item);
7674 obs_sceneitem_set_info(item, &copiedTransformInfo);
7675 obs_sceneitem_set_crop(item, &copiedCropInfo);
7676 obs_sceneitem_defer_update_end(item);
7677
7678 UNUSED_PARAMETER(scene);
7679 UNUSED_PARAMETER(param);
7680 return true;
7681 };
7682
7683 obs_scene_enum_items(GetCurrentScene(), func, nullptr);
7684
7685 obs_data_t *rwrapper =
7686 obs_scene_save_transform_states(GetCurrentScene(), false);
7687
7688 std::string undo_data(obs_data_get_json(wrapper));
7689 std::string redo_data(obs_data_get_json(rwrapper));
7690 undo_s.add_action(
7691 QTStr("Undo.Transform.Paste")
7692 .arg(obs_source_get_name(GetCurrentSceneSource())),
7693 undo_redo, undo_redo, undo_data, redo_data);
7694
7695 obs_data_release(wrapper);
7696 obs_data_release(rwrapper);
7697 }
7698
7699 static bool reset_tr(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
7700 {
7701 if (obs_sceneitem_is_group(item))
7702 obs_sceneitem_group_enum_items(item, reset_tr, nullptr);
7703 if (!obs_sceneitem_selected(item))
7704 return true;
7705 if (obs_sceneitem_locked(item))
7706 return true;
7707
7708 obs_sceneitem_defer_update_begin(item);
7709
7710 obs_transform_info info;
7711 vec2_set(&info.pos, 0.0f, 0.0f);
7712 vec2_set(&info.scale, 1.0f, 1.0f);
7713 info.rot = 0.0f;
7714 info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT;
7715 info.bounds_type = OBS_BOUNDS_NONE;
7716 info.bounds_alignment = OBS_ALIGN_CENTER;
7717 vec2_set(&info.bounds, 0.0f, 0.0f);
7718 obs_sceneitem_set_info(item, &info);
7719
7720 obs_sceneitem_crop crop = {};
7721 obs_sceneitem_set_crop(item, &crop);
7722
7723 obs_sceneitem_defer_update_end(item);
7724
7725 UNUSED_PARAMETER(scene);
7726 UNUSED_PARAMETER(param);
7727 return true;
7728 }
7729
7730 void OBSBasic::on_actionResetTransform_triggered()
7731 {
7732 obs_scene_t *scene = GetCurrentScene();
7733
7734 obs_data_t *wrapper = obs_scene_save_transform_states(scene, false);
7735 obs_scene_enum_items(scene, reset_tr, nullptr);
7736 obs_data_t *rwrapper = obs_scene_save_transform_states(scene, false);
7737
7738 std::string undo_data(obs_data_get_json(wrapper));
7739 std::string redo_data(obs_data_get_json(rwrapper));
7740 undo_s.add_action(
7741 QTStr("Undo.Transform.Reset")
7742 .arg(obs_source_get_name(obs_scene_get_source(scene))),
7743 undo_redo, undo_redo, undo_data, redo_data);
7744
7745 obs_data_release(wrapper);
7746 obs_data_release(rwrapper);
7747
7748 obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr);
7749 }
7750
7751 static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br)
7752 {
7753 matrix4 boxTransform;
7754 obs_sceneitem_get_box_transform(item, &boxTransform);
7755
7756 vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f);
7757 vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f);
7758
7759 auto GetMinPos = [&](float x, float y) {
7760 vec3 pos;
7761 vec3_set(&pos, x, y, 0.0f);
7762 vec3_transform(&pos, &pos, &boxTransform);
7763 vec3_min(&tl, &tl, &pos);
7764 vec3_max(&br, &br, &pos);
7765 };
7766
7767 GetMinPos(0.0f, 0.0f);
7768 GetMinPos(1.0f, 0.0f);
7769 GetMinPos(0.0f, 1.0f);
7770 GetMinPos(1.0f, 1.0f);
7771 }
7772
7773 static vec3 GetItemTL(obs_sceneitem_t *item)
7774 {
7775 vec3 tl, br;
7776 GetItemBox(item, tl, br);
7777 return tl;
7778 }
7779
7780 static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl)
7781 {
7782 vec3 newTL;
7783 vec2 pos;
7784
7785 obs_sceneitem_get_pos(item, &pos);
7786 newTL = GetItemTL(item);
7787 pos.x += tl.x - newTL.x;
7788 pos.y += tl.y - newTL.y;
7789 obs_sceneitem_set_pos(item, &pos);
7790 }
7791
7792 static bool RotateSelectedSources(obs_scene_t *scene, obs_sceneitem_t *item,
7793 void *param)
7794 {
7795 if (obs_sceneitem_is_group(item))
7796 obs_sceneitem_group_enum_items(item, RotateSelectedSources,
7797 param);
7798 if (!obs_sceneitem_selected(item))
7799 return true;
7800 if (obs_sceneitem_locked(item))
7801 return true;
7802
7803 float rot = *reinterpret_cast<float *>(param);
7804
7805 vec3 tl = GetItemTL(item);
7806
7807 rot += obs_sceneitem_get_rot(item);
7808 if (rot >= 360.0f)
7809 rot -= 360.0f;
7810 else if (rot <= -360.0f)
7811 rot += 360.0f;
7812 obs_sceneitem_set_rot(item, rot);
7813
7814 obs_sceneitem_force_update_transform(item);
7815
7816 SetItemTL(item, tl);
7817
7818 UNUSED_PARAMETER(scene);
7819 return true;
7820 };
7821
7822 void OBSBasic::on_actionRotate90CW_triggered()
7823 {
7824 float f90CW = 90.0f;
7825 obs_data_t *wrapper =
7826 obs_scene_save_transform_states(GetCurrentScene(), false);
7827 obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW);
7828 obs_data_t *rwrapper =
7829 obs_scene_save_transform_states(GetCurrentScene(), false);
7830
7831 std::string undo_data(obs_data_get_json(wrapper));
7832 std::string redo_data(obs_data_get_json(rwrapper));
7833 undo_s.add_action(QTStr("Undo.Transform.Rotate")
7834 .arg(obs_source_get_name(obs_scene_get_source(
7835 GetCurrentScene()))),
7836 undo_redo, undo_redo, undo_data, redo_data);
7837
7838 obs_data_release(wrapper);
7839 obs_data_release(rwrapper);
7840 }
7841
7842 void OBSBasic::on_actionRotate90CCW_triggered()
7843 {
7844 float f90CCW = -90.0f;
7845 obs_data_t *wrapper =
7846 obs_scene_save_transform_states(GetCurrentScene(), false);
7847 obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW);
7848 obs_data_t *rwrapper =
7849 obs_scene_save_transform_states(GetCurrentScene(), false);
7850
7851 std::string undo_data(obs_data_get_json(wrapper));
7852 std::string redo_data(obs_data_get_json(rwrapper));
7853 undo_s.add_action(QTStr("Undo.Transform.Rotate")
7854 .arg(obs_source_get_name(obs_scene_get_source(
7855 GetCurrentScene()))),
7856 undo_redo, undo_redo, undo_data, redo_data);
7857
7858 obs_data_release(wrapper);
7859 obs_data_release(rwrapper);
7860 }
7861
7862 void OBSBasic::on_actionRotate180_triggered()
7863 {
7864 float f180 = 180.0f;
7865 obs_data_t *wrapper =
7866 obs_scene_save_transform_states(GetCurrentScene(), false);
7867 obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180);
7868 obs_data_t *rwrapper =
7869 obs_scene_save_transform_states(GetCurrentScene(), false);
7870
7871 std::string undo_data(obs_data_get_json(wrapper));
7872 std::string redo_data(obs_data_get_json(rwrapper));
7873 undo_s.add_action(QTStr("Undo.Transform.Rotate")
7874 .arg(obs_source_get_name(obs_scene_get_source(
7875 GetCurrentScene()))),
7876 undo_redo, undo_redo, undo_data, redo_data);
7877
7878 obs_data_release(wrapper);
7879 obs_data_release(rwrapper);
7880 }
7881
7882 static bool MultiplySelectedItemScale(obs_scene_t *scene, obs_sceneitem_t *item,
7883 void *param)
7884 {
7885 vec2 &mul = *reinterpret_cast<vec2 *>(param);
7886
7887 if (obs_sceneitem_is_group(item))
7888 obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale,
7889 param);
7890 if (!obs_sceneitem_selected(item))
7891 return true;
7892 if (obs_sceneitem_locked(item))
7893 return true;
7894
7895 vec3 tl = GetItemTL(item);
7896
7897 vec2 scale;
7898 obs_sceneitem_get_scale(item, &scale);
7899 vec2_mul(&scale, &scale, &mul);
7900 obs_sceneitem_set_scale(item, &scale);
7901
7902 obs_sceneitem_force_update_transform(item);
7903
7904 SetItemTL(item, tl);
7905
7906 UNUSED_PARAMETER(scene);
7907 return true;
7908 }
7909
7910 void OBSBasic::on_actionFlipHorizontal_triggered()
7911 {
7912 vec2 scale;
7913 vec2_set(&scale, -1.0f, 1.0f);
7914 obs_data_t *wrapper =
7915 obs_scene_save_transform_states(GetCurrentScene(), false);
7916 obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale,
7917 &scale);
7918 obs_data_t *rwrapper =
7919 obs_scene_save_transform_states(GetCurrentScene(), false);
7920
7921 std::string undo_data(obs_data_get_json(wrapper));
7922 std::string redo_data(obs_data_get_json(rwrapper));
7923 undo_s.add_action(QTStr("Undo.Transform.HFlip")
7924 .arg(obs_source_get_name(obs_scene_get_source(
7925 GetCurrentScene()))),
7926 undo_redo, undo_redo, undo_data, redo_data);
7927
7928 obs_data_release(wrapper);
7929 obs_data_release(rwrapper);
7930 }
7931
7932 void OBSBasic::on_actionFlipVertical_triggered()
7933 {
7934 vec2 scale;
7935 vec2_set(&scale, 1.0f, -1.0f);
7936 obs_data_t *wrapper =
7937 obs_scene_save_transform_states(GetCurrentScene(), false);
7938 obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale,
7939 &scale);
7940 obs_data_t *rwrapper =
7941 obs_scene_save_transform_states(GetCurrentScene(), false);
7942
7943 std::string undo_data(obs_data_get_json(wrapper));
7944 std::string redo_data(obs_data_get_json(rwrapper));
7945 undo_s.add_action(QTStr("Undo.Transform.VFlip")
7946 .arg(obs_source_get_name(obs_scene_get_source(
7947 GetCurrentScene()))),
7948 undo_redo, undo_redo, undo_data, redo_data);
7949
7950 obs_data_release(wrapper);
7951 obs_data_release(rwrapper);
7952 }
7953
7954 static bool CenterAlignSelectedItems(obs_scene_t *scene, obs_sceneitem_t *item,
7955 void *param)
7956 {
7957 obs_bounds_type boundsType =
7958 *reinterpret_cast<obs_bounds_type *>(param);
7959
7960 if (obs_sceneitem_is_group(item))
7961 obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems,
7962 param);
7963 if (!obs_sceneitem_selected(item))
7964 return true;
7965 if (obs_sceneitem_locked(item))
7966 return true;
7967
7968 obs_video_info ovi;
7969 obs_get_video_info(&ovi);
7970
7971 obs_transform_info itemInfo;
7972 vec2_set(&itemInfo.pos, 0.0f, 0.0f);
7973 vec2_set(&itemInfo.scale, 1.0f, 1.0f);
7974 itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP;
7975 itemInfo.rot = 0.0f;
7976
7977 vec2_set(&itemInfo.bounds, float(ovi.base_width),
7978 float(ovi.base_height));
7979 itemInfo.bounds_type = boundsType;
7980 itemInfo.bounds_alignment = OBS_ALIGN_CENTER;
7981
7982 obs_sceneitem_set_info(item, &itemInfo);
7983
7984 UNUSED_PARAMETER(scene);
7985 return true;
7986 }
7987
7988 void OBSBasic::on_actionFitToScreen_triggered()
7989 {
7990 obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER;
7991 obs_data_t *wrapper =
7992 obs_scene_save_transform_states(GetCurrentScene(), false);
7993 obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems,
7994 &boundsType);
7995 obs_data_t *rwrapper =
7996 obs_scene_save_transform_states(GetCurrentScene(), false);
7997
7998 std::string undo_data(obs_data_get_json(wrapper));
7999 std::string redo_data(obs_data_get_json(rwrapper));
8000 undo_s.add_action(QTStr("Undo.Transform.FitToScreen")
8001 .arg(obs_source_get_name(obs_scene_get_source(
8002 GetCurrentScene()))),
8003 undo_redo, undo_redo, undo_data, redo_data);
8004
8005 obs_data_release(wrapper);
8006 obs_data_release(rwrapper);
8007 }
8008
8009 void OBSBasic::on_actionStretchToScreen_triggered()
8010 {
8011 obs_bounds_type boundsType = OBS_BOUNDS_STRETCH;
8012 obs_data_t *wrapper =
8013 obs_scene_save_transform_states(GetCurrentScene(), false);
8014 obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems,
8015 &boundsType);
8016 obs_data_t *rwrapper =
8017 obs_scene_save_transform_states(GetCurrentScene(), false);
8018
8019 std::string undo_data(obs_data_get_json(wrapper));
8020 std::string redo_data(obs_data_get_json(rwrapper));
8021 undo_s.add_action(QTStr("Undo.Transform.StretchToScreen")
8022 .arg(obs_source_get_name(obs_scene_get_source(
8023 GetCurrentScene()))),
8024 undo_redo, undo_redo, undo_data, redo_data);
8025
8026 obs_data_release(wrapper);
8027 obs_data_release(rwrapper);
8028 }
8029
8030 enum class CenterType {
8031 Scene,
8032 Vertical,
8033 Horizontal,
8034 };
8035
8036 static bool center_to_scene(obs_scene_t *, obs_sceneitem_t *item, void *param)
8037 {
8038 CenterType centerType = *reinterpret_cast<CenterType *>(param);
8039
8040 vec3 tl, br, itemCenter, screenCenter, offset;
8041 obs_video_info ovi;
8042 obs_transform_info oti;
8043
8044 if (obs_sceneitem_is_group(item))
8045 obs_sceneitem_group_enum_items(item, center_to_scene,
8046 ¢erType);
8047 if (!obs_sceneitem_selected(item))
8048 return true;
8049 if (obs_sceneitem_locked(item))
8050 return true;
8051
8052 obs_get_video_info(&ovi);
8053 obs_sceneitem_get_info(item, &oti);
8054
8055 if (centerType == CenterType::Scene)
8056 vec3_set(&screenCenter, float(ovi.base_width),
8057 float(ovi.base_height), 0.0f);
8058 else if (centerType == CenterType::Vertical)
8059 vec3_set(&screenCenter, float(oti.bounds.x),
8060 float(ovi.base_height), 0.0f);
8061 else if (centerType == CenterType::Horizontal)
8062 vec3_set(&screenCenter, float(ovi.base_width),
8063 float(oti.bounds.y), 0.0f);
8064
8065 vec3_mulf(&screenCenter, &screenCenter, 0.5f);
8066
8067 GetItemBox(item, tl, br);
8068
8069 vec3_sub(&itemCenter, &br, &tl);
8070 vec3_mulf(&itemCenter, &itemCenter, 0.5f);
8071 vec3_add(&itemCenter, &itemCenter, &tl);
8072
8073 vec3_sub(&offset, &screenCenter, &itemCenter);
8074 vec3_add(&tl, &tl, &offset);
8075
8076 if (centerType == CenterType::Vertical)
8077 tl.x = oti.pos.x;
8078 else if (centerType == CenterType::Horizontal)
8079 tl.y = oti.pos.y;
8080
8081 SetItemTL(item, tl);
8082 return true;
8083 };
8084
8085 void OBSBasic::on_actionCenterToScreen_triggered()
8086 {
8087 CenterType centerType = CenterType::Scene;
8088 obs_data_t *wrapper =
8089 obs_scene_save_transform_states(GetCurrentScene(), false);
8090 obs_scene_enum_items(GetCurrentScene(), center_to_scene, ¢erType);
8091 obs_data_t *rwrapper =
8092 obs_scene_save_transform_states(GetCurrentScene(), false);
8093
8094 std::string undo_data(obs_data_get_json(wrapper));
8095 std::string redo_data(obs_data_get_json(rwrapper));
8096 undo_s.add_action(QTStr("Undo.Transform.Center")
8097 .arg(obs_source_get_name(obs_scene_get_source(
8098 GetCurrentScene()))),
8099 undo_redo, undo_redo, undo_data, redo_data);
8100
8101 obs_data_release(wrapper);
8102 obs_data_release(rwrapper);
8103 }
8104
8105 void OBSBasic::on_actionVerticalCenter_triggered()
8106 {
8107 CenterType centerType = CenterType::Vertical;
8108 obs_data_t *wrapper =
8109 obs_scene_save_transform_states(GetCurrentScene(), false);
8110 obs_scene_enum_items(GetCurrentScene(), center_to_scene, ¢erType);
8111 obs_data_t *rwrapper =
8112 obs_scene_save_transform_states(GetCurrentScene(), false);
8113
8114 std::string undo_data(obs_data_get_json(wrapper));
8115 std::string redo_data(obs_data_get_json(rwrapper));
8116 undo_s.add_action(QTStr("Undo.Transform.VCenter")
8117 .arg(obs_source_get_name(obs_scene_get_source(
8118 GetCurrentScene()))),
8119 undo_redo, undo_redo, undo_data, redo_data);
8120
8121 obs_data_release(wrapper);
8122 obs_data_release(rwrapper);
8123 }
8124
8125 void OBSBasic::on_actionHorizontalCenter_triggered()
8126 {
8127 CenterType centerType = CenterType::Horizontal;
8128 obs_data_t *wrapper =
8129 obs_scene_save_transform_states(GetCurrentScene(), false);
8130 obs_scene_enum_items(GetCurrentScene(), center_to_scene, ¢erType);
8131 obs_data_t *rwrapper =
8132 obs_scene_save_transform_states(GetCurrentScene(), false);
8133
8134 std::string undo_data(obs_data_get_json(wrapper));
8135 std::string redo_data(obs_data_get_json(rwrapper));
8136 undo_s.add_action(QTStr("Undo.Transform.HCenter")
8137 .arg(obs_source_get_name(obs_scene_get_source(
8138 GetCurrentScene()))),
8139 undo_redo, undo_redo, undo_data, redo_data);
8140
8141 obs_data_release(wrapper);
8142 obs_data_release(rwrapper);
8143 }
8144
8145 void OBSBasic::EnablePreviewDisplay(bool enable)
8146 {
8147 obs_display_set_enabled(ui->preview->GetDisplay(), enable);
8148 ui->preview->setVisible(enable);
8149 ui->previewDisabledWidget->setVisible(!enable);
8150 }
8151
8152 void OBSBasic::TogglePreview()
8153 {
8154 previewEnabled = !previewEnabled;
8155 EnablePreviewDisplay(previewEnabled);
8156 }
8157
8158 void OBSBasic::EnablePreview()
8159 {
8160 if (previewProgramMode)
8161 return;
8162
8163 previewEnabled = true;
8164 EnablePreviewDisplay(true);
8165 }
8166
8167 void OBSBasic::DisablePreview()
8168 {
8169 if (previewProgramMode)
8170 return;
8171
8172 previewEnabled = false;
8173 EnablePreviewDisplay(false);
8174 }
8175
8176 static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param)
8177 {
8178 if (obs_sceneitem_locked(item))
8179 return true;
8180
8181 struct vec2 &offset = *reinterpret_cast<struct vec2 *>(param);
8182 struct vec2 pos;
8183
8184 if (!obs_sceneitem_selected(item)) {
8185 if (obs_sceneitem_is_group(item)) {
8186 struct vec3 offset3;
8187 vec3_set(&offset3, offset.x, offset.y, 0.0f);
8188
8189 struct matrix4 matrix;
8190 obs_sceneitem_get_draw_transform(item, &matrix);
8191 vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f);
8192 matrix4_inv(&matrix, &matrix);
8193 vec3_transform(&offset3, &offset3, &matrix);
8194
8195 struct vec2 new_offset;
8196 vec2_set(&new_offset, offset3.x, offset3.y);
8197 obs_sceneitem_group_enum_items(item, nudge_callback,
8198 &new_offset);
8199 }
8200
8201 return true;
8202 }
8203
8204 obs_sceneitem_get_pos(item, &pos);
8205 vec2_add(&pos, &pos, &offset);
8206 obs_sceneitem_set_pos(item, &pos);
8207 return true;
8208 }
8209
8210 void OBSBasic::Nudge(int dist, MoveDir dir)
8211 {
8212 if (ui->preview->Locked())
8213 return;
8214
8215 struct vec2 offset;
8216 vec2_set(&offset, 0.0f, 0.0f);
8217
8218 switch (dir) {
8219 case MoveDir::Up:
8220 offset.y = (float)-dist;
8221 break;
8222 case MoveDir::Down:
8223 offset.y = (float)dist;
8224 break;
8225 case MoveDir::Left:
8226 offset.x = (float)-dist;
8227 break;
8228 case MoveDir::Right:
8229 offset.x = (float)dist;
8230 break;
8231 }
8232
8233 if (!recent_nudge) {
8234 recent_nudge = true;
8235 obs_data_t *wrapper = obs_scene_save_transform_states(
8236 GetCurrentScene(), true);
8237 std::string undo_data(obs_data_get_json(wrapper));
8238
8239 nudge_timer = new QTimer;
8240 QObject::connect(
8241 nudge_timer, &QTimer::timeout,
8242 [this, &recent_nudge = recent_nudge, undo_data]() {
8243 obs_data_t *rwrapper =
8244 obs_scene_save_transform_states(
8245 GetCurrentScene(), true);
8246 std::string redo_data(
8247 obs_data_get_json(rwrapper));
8248
8249 undo_s.add_action(
8250 QTStr("Undo.Transform")
8251 .arg(obs_source_get_name(
8252 GetCurrentSceneSource())),
8253 undo_redo, undo_redo, undo_data,
8254 redo_data);
8255
8256 recent_nudge = false;
8257 obs_data_release(rwrapper);
8258 });
8259 connect(nudge_timer, &QTimer::timeout, nudge_timer,
8260 &QTimer::deleteLater);
8261 nudge_timer->setSingleShot(true);
8262
8263 obs_data_release(wrapper);
8264 }
8265
8266 if (nudge_timer) {
8267 nudge_timer->stop();
8268 nudge_timer->start(1000);
8269 } else {
8270 blog(LOG_ERROR, "No nudge timer!");
8271 }
8272
8273 obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset);
8274 }
8275
8276 void OBSBasic::NudgeUp()
8277 {
8278 Nudge(1, MoveDir::Up);
8279 }
8280 void OBSBasic::NudgeDown()
8281 {
8282 Nudge(1, MoveDir::Down);
8283 }
8284 void OBSBasic::NudgeLeft()
8285 {
8286 Nudge(1, MoveDir::Left);
8287 }
8288 void OBSBasic::NudgeRight()
8289 {
8290 Nudge(1, MoveDir::Right);
8291 }
8292
8293 void OBSBasic::DeleteProjector(OBSProjector *projector)
8294 {
8295 for (size_t i = 0; i < projectors.size(); i++) {
8296 if (projectors[i] == projector) {
8297 projectors[i]->deleteLater();
8298 projectors.erase(projectors.begin() + i);
8299 break;
8300 }
8301 }
8302 }
8303
8304 OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor,
8305 ProjectorType type)
8306 {
8307 /* seriously? 10 monitors? */
8308 if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1)
8309 return nullptr;
8310
8311 OBSProjector *projector =
8312 new OBSProjector(nullptr, source, monitor, type);
8313
8314 projectors.emplace_back(projector);
8315
8316 return projector;
8317 }
8318
8319 void OBSBasic::OpenStudioProgramProjector()
8320 {
8321 int monitor = sender()->property("monitor").toInt();
8322 OpenProjector(nullptr, monitor, ProjectorType::StudioProgram);
8323 }
8324
8325 void OBSBasic::OpenPreviewProjector()
8326 {
8327 int monitor = sender()->property("monitor").toInt();
8328 OpenProjector(nullptr, monitor, ProjectorType::Preview);
8329 }
8330
8331 void OBSBasic::OpenSourceProjector()
8332 {
8333 int monitor = sender()->property("monitor").toInt();
8334 OBSSceneItem item = GetCurrentSceneItem();
8335 if (!item)
8336 return;
8337
8338 OpenProjector(obs_sceneitem_get_source(item), monitor,
8339 ProjectorType::Source);
8340 }
8341
8342 void OBSBasic::OpenMultiviewProjector()
8343 {
8344 int monitor = sender()->property("monitor").toInt();
8345 OpenProjector(nullptr, monitor, ProjectorType::Multiview);
8346 }
8347
8348 void OBSBasic::OpenSceneProjector()
8349 {
8350 int monitor = sender()->property("monitor").toInt();
8351 OBSScene scene = GetCurrentScene();
8352 if (!scene)
8353 return;
8354
8355 OpenProjector(obs_scene_get_source(scene), monitor,
8356 ProjectorType::Scene);
8357 }
8358
8359 void OBSBasic::OpenStudioProgramWindow()
8360 {
8361 OpenProjector(nullptr, -1, ProjectorType::StudioProgram);
8362 }
8363
8364 void OBSBasic::OpenPreviewWindow()
8365 {
8366 OpenProjector(nullptr, -1, ProjectorType::Preview);
8367 }
8368
8369 void OBSBasic::OpenSourceWindow()
8370 {
8371 OBSSceneItem item = GetCurrentSceneItem();
8372 if (!item)
8373 return;
8374
8375 OBSSource source = obs_sceneitem_get_source(item);
8376
8377 OpenProjector(obs_sceneitem_get_source(item), -1,
8378 ProjectorType::Source);
8379 }
8380
8381 void OBSBasic::OpenMultiviewWindow()
8382 {
8383 OpenProjector(nullptr, -1, ProjectorType::Multiview);
8384 }
8385
8386 void OBSBasic::OpenSceneWindow()
8387 {
8388 OBSScene scene = GetCurrentScene();
8389 if (!scene)
8390 return;
8391
8392 OBSSource source = obs_scene_get_source(scene);
8393
8394 OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene);
8395 }
8396
8397 void OBSBasic::OpenSavedProjectors()
8398 {
8399 for (SavedProjectorInfo *info : savedProjectorsArray) {
8400 OpenSavedProjector(info);
8401 }
8402 }
8403
8404 void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info)
8405 {
8406 if (info) {
8407 OBSProjector *projector = nullptr;
8408 switch (info->type) {
8409 case ProjectorType::Source:
8410 case ProjectorType::Scene: {
8411 OBSSource source =
8412 obs_get_source_by_name(info->name.c_str());
8413 if (!source)
8414 return;
8415
8416 projector = OpenProjector(source, info->monitor,
8417 info->type);
8418
8419 obs_source_release(source);
8420 break;
8421 }
8422 default: {
8423 projector = OpenProjector(nullptr, info->monitor,
8424 info->type);
8425 break;
8426 }
8427 }
8428
8429 if (projector && !info->geometry.empty() && info->monitor < 0) {
8430 QByteArray byteArray = QByteArray::fromBase64(
8431 QByteArray(info->geometry.c_str()));
8432 projector->restoreGeometry(byteArray);
8433
8434 if (!WindowPositionValid(projector->normalGeometry())) {
8435 QRect rect = QGuiApplication::primaryScreen()
8436 ->geometry();
8437 projector->setGeometry(QStyle::alignedRect(
8438 Qt::LeftToRight, Qt::AlignCenter,
8439 size(), rect));
8440 }
8441
8442 if (info->alwaysOnTopOverridden)
8443 projector->SetIsAlwaysOnTop(info->alwaysOnTop,
8444 true);
8445 }
8446 }
8447 }
8448
8449 void OBSBasic::on_actionFullscreenInterface_triggered()
8450 {
8451 if (!isFullScreen())
8452 showFullScreen();
8453 else
8454 showNormal();
8455 }
8456
8457 void OBSBasic::UpdateTitleBar()
8458 {
8459 stringstream name;
8460
8461 const char *profile =
8462 config_get_string(App()->GlobalConfig(), "Basic", "Profile");
8463 const char *sceneCollection = config_get_string(
8464 App()->GlobalConfig(), "Basic", "SceneCollection");
8465
8466 name << "OBS ";
8467 if (previewProgramMode)
8468 name << "Studio ";
8469
8470 name << App()->GetVersionString();
8471 if (App()->IsPortableMode())
8472 name << " - Portable Mode";
8473
8474 name << " - " << Str("TitleBar.Profile") << ": " << profile;
8475 name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection;
8476
8477 setWindowTitle(QT_UTF8(name.str().c_str()));
8478 }
8479
8480 int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const
8481 {
8482 char profiles_path[512];
8483 const char *profile =
8484 config_get_string(App()->GlobalConfig(), "Basic", "ProfileDir");
8485 int ret;
8486
8487 if (!profile)
8488 return -1;
8489 if (!path)
8490 return -1;
8491 if (!file)
8492 file = "";
8493
8494 ret = GetConfigPath(profiles_path, 512, "obs-studio/basic/profiles");
8495 if (ret <= 0)
8496 return ret;
8497
8498 if (!*file)
8499 return snprintf(path, size, "%s/%s", profiles_path, profile);
8500
8501 return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file);
8502 }
8503
8504 void OBSBasic::on_resetUI_triggered()
8505 {
8506 /* prune deleted extra docks */
8507 for (int i = extraDocks.size() - 1; i >= 0; i--) {
8508 if (!extraDocks[i]) {
8509 extraDocks.removeAt(i);
8510 }
8511 }
8512
8513 if (extraDocks.size()) {
8514 QMessageBox::StandardButton button = QMessageBox::question(
8515 this, QTStr("ResetUIWarning.Title"),
8516 QTStr("ResetUIWarning.Text"));
8517
8518 if (button == QMessageBox::No)
8519 return;
8520 }
8521
8522 /* undock/hide/center extra docks */
8523 for (int i = extraDocks.size() - 1; i >= 0; i--) {
8524 if (extraDocks[i]) {
8525 extraDocks[i]->setVisible(true);
8526 extraDocks[i]->setFloating(true);
8527 extraDocks[i]->move(frameGeometry().topLeft() +
8528 rect().center() -
8529 extraDocks[i]->rect().center());
8530 extraDocks[i]->setVisible(false);
8531 }
8532 }
8533
8534 restoreState(startingDockLayout);
8535
8536 int cx = width();
8537 int cy = height();
8538
8539 int cx22_5 = cx * 225 / 1000;
8540 int cx5 = cx * 5 / 100;
8541
8542 cy = cy * 225 / 1000;
8543
8544 int mixerSize = cx - (cx22_5 * 2 + cx5 * 2);
8545
8546 QList<QDockWidget *> docks{ui->scenesDock, ui->sourcesDock,
8547 ui->mixerDock, ui->transitionsDock,
8548 ui->controlsDock};
8549
8550 QList<int> sizes{cx22_5, cx22_5, mixerSize, cx5, cx5};
8551
8552 ui->scenesDock->setVisible(true);
8553 ui->sourcesDock->setVisible(true);
8554 ui->mixerDock->setVisible(true);
8555 ui->transitionsDock->setVisible(true);
8556 ui->controlsDock->setVisible(true);
8557 statsDock->setVisible(false);
8558 statsDock->setFloating(true);
8559
8560 resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical);
8561 resizeDocks(docks, sizes, Qt::Horizontal);
8562
8563 activateWindow();
8564 }
8565
8566 void OBSBasic::on_lockUI_toggled(bool lock)
8567 {
8568 QDockWidget::DockWidgetFeatures features =
8569 lock ? QDockWidget::NoDockWidgetFeatures
8570 : (QDockWidget::DockWidgetClosable |
8571 QDockWidget::DockWidgetMovable |
8572 QDockWidget::DockWidgetFloatable);
8573
8574 QDockWidget::DockWidgetFeatures mainFeatures = features;
8575 mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable;
8576
8577 ui->scenesDock->setFeatures(mainFeatures);
8578 ui->sourcesDock->setFeatures(mainFeatures);
8579 ui->mixerDock->setFeatures(mainFeatures);
8580 ui->transitionsDock->setFeatures(mainFeatures);
8581 ui->controlsDock->setFeatures(mainFeatures);
8582 statsDock->setFeatures(features);
8583
8584 for (int i = extraDocks.size() - 1; i >= 0; i--) {
8585 if (!extraDocks[i]) {
8586 extraDocks.removeAt(i);
8587 } else {
8588 extraDocks[i]->setFeatures(features);
8589 }
8590 }
8591 }
8592
8593 void OBSBasic::on_toggleListboxToolbars_toggled(bool visible)
8594 {
8595 ui->sourcesToolbar->setVisible(visible);
8596 ui->scenesToolbar->setVisible(visible);
8597
8598 config_set_bool(App()->GlobalConfig(), "BasicWindow",
8599 "ShowListboxToolbars", visible);
8600 }
8601
8602 void OBSBasic::ShowContextBar()
8603 {
8604 on_toggleContextBar_toggled(true);
8605 }
8606
8607 void OBSBasic::HideContextBar()
8608 {
8609 on_toggleContextBar_toggled(false);
8610 }
8611
8612 void OBSBasic::on_toggleContextBar_toggled(bool visible)
8613 {
8614 config_set_bool(App()->GlobalConfig(), "BasicWindow",
8615 "ShowContextToolbars", visible);
8616 this->ui->contextContainer->setVisible(visible);
8617 UpdateContextBar(true);
8618 }
8619
8620 void OBSBasic::on_toggleStatusBar_toggled(bool visible)
8621 {
8622 ui->statusbar->setVisible(visible);
8623
8624 config_set_bool(App()->GlobalConfig(), "BasicWindow", "ShowStatusBar",
8625 visible);
8626 }
8627
8628 void OBSBasic::on_toggleSourceIcons_toggled(bool visible)
8629 {
8630 ui->sources->SetIconsVisible(visible);
8631 if (advAudioWindow != nullptr)
8632 advAudioWindow->SetIconsVisible(visible);
8633
8634 config_set_bool(App()->GlobalConfig(), "BasicWindow", "ShowSourceIcons",
8635 visible);
8636 }
8637
8638 void OBSBasic::on_actionLockPreview_triggered()
8639 {
8640 ui->preview->ToggleLocked();
8641 ui->actionLockPreview->setChecked(ui->preview->Locked());
8642 }
8643
8644 void OBSBasic::on_scalingMenu_aboutToShow()
8645 {
8646 obs_video_info ovi;
8647 obs_get_video_info(&ovi);
8648
8649 QAction *action = ui->actionScaleCanvas;
8650 QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas");
8651 text = text.arg(QString::number(ovi.base_width),
8652 QString::number(ovi.base_height));
8653 action->setText(text);
8654
8655 action = ui->actionScaleOutput;
8656 text = QTStr("Basic.MainMenu.Edit.Scale.Output");
8657 text = text.arg(QString::number(ovi.output_width),
8658 QString::number(ovi.output_height));
8659 action->setText(text);
8660 action->setVisible(!(ovi.output_width == ovi.base_width &&
8661 ovi.output_height == ovi.base_height));
8662
8663 UpdatePreviewScalingMenu();
8664 }
8665
8666 void OBSBasic::on_actionScaleWindow_triggered()
8667 {
8668 ui->preview->SetFixedScaling(false);
8669 ui->preview->ResetScrollingOffset();
8670 emit ui->preview->DisplayResized();
8671 }
8672
8673 void OBSBasic::on_actionScaleCanvas_triggered()
8674 {
8675 ui->preview->SetFixedScaling(true);
8676 ui->preview->SetScalingLevel(0);
8677 emit ui->preview->DisplayResized();
8678 }
8679
8680 void OBSBasic::on_actionScaleOutput_triggered()
8681 {
8682 obs_video_info ovi;
8683 obs_get_video_info(&ovi);
8684
8685 ui->preview->SetFixedScaling(true);
8686 float scalingAmount = float(ovi.output_width) / float(ovi.base_width);
8687 // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY)
8688 int32_t approxScalingLevel =
8689 int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY)));
8690 ui->preview->SetScalingLevel(approxScalingLevel);
8691 ui->preview->SetScalingAmount(scalingAmount);
8692 emit ui->preview->DisplayResized();
8693 }
8694
8695 void OBSBasic::SetShowing(bool showing)
8696 {
8697 if (!showing && isVisible()) {
8698 config_set_string(App()->GlobalConfig(), "BasicWindow",
8699 "geometry",
8700 saveGeometry().toBase64().constData());
8701
8702 /* hide all visible child dialogs */
8703 visDlgPositions.clear();
8704 if (!visDialogs.isEmpty()) {
8705 for (QDialog *dlg : visDialogs) {
8706 visDlgPositions.append(dlg->pos());
8707 dlg->hide();
8708 }
8709 }
8710
8711 if (showHide)
8712 showHide->setText(QTStr("Basic.SystemTray.Show"));
8713 QTimer::singleShot(250, this, SLOT(hide()));
8714
8715 setVisible(false);
8716
8717 #ifdef __APPLE__
8718 EnableOSXDockIcon(false);
8719 #endif
8720
8721 } else if (showing && !isVisible()) {
8722 if (showHide)
8723 showHide->setText(QTStr("Basic.SystemTray.Hide"));
8724 QTimer::singleShot(250, this, SLOT(show()));
8725
8726 setVisible(true);
8727
8728 #ifdef __APPLE__
8729 EnableOSXDockIcon(true);
8730 #endif
8731
8732 /* raise and activate window to ensure it is on top */
8733 raise();
8734 activateWindow();
8735
8736 /* show all child dialogs that was visible earlier */
8737 if (!visDialogs.isEmpty()) {
8738 for (int i = 0; i < visDialogs.size(); ++i) {
8739 QDialog *dlg = visDialogs[i];
8740 dlg->move(visDlgPositions[i]);
8741 dlg->show();
8742 }
8743 }
8744
8745 /* Unminimize window if it was hidden to tray instead of task
8746 * bar. */
8747 if (sysTrayMinimizeToTray()) {
8748 Qt::WindowStates state;
8749 state = windowState() & ~Qt::WindowMinimized;
8750 state |= Qt::WindowActive;
8751 setWindowState(state);
8752 }
8753 }
8754 }
8755
8756 void OBSBasic::ToggleShowHide()
8757 {
8758 bool showing = isVisible();
8759 if (showing) {
8760 /* check for modal dialogs */
8761 EnumDialogs();
8762 if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty())
8763 return;
8764 }
8765 SetShowing(!showing);
8766 }
8767
8768 void OBSBasic::SystemTrayInit()
8769 {
8770 #ifdef __APPLE__
8771 QIcon trayIconFile = QIcon(":/res/images/obs_macos.png");
8772 trayIconFile.setIsMask(true);
8773 #else
8774 QIcon trayIconFile = QIcon(":/res/images/obs.png");
8775 #endif
8776 trayIcon.reset(new QSystemTrayIcon(
8777 QIcon::fromTheme("obs-tray", trayIconFile), this));
8778 trayIcon->setToolTip("OBS Studio");
8779
8780 showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data());
8781 sysTrayStream = new QAction(QTStr("Basic.Main.StartStreaming"),
8782 trayIcon.data());
8783 sysTrayRecord = new QAction(QTStr("Basic.Main.StartRecording"),
8784 trayIcon.data());
8785 sysTrayReplayBuffer = new QAction(QTStr("Basic.Main.StartReplayBuffer"),
8786 trayIcon.data());
8787 sysTrayVirtualCam = new QAction(QTStr("Basic.Main.StartVirtualCam"),
8788 trayIcon.data());
8789 exit = new QAction(QTStr("Exit"), trayIcon.data());
8790
8791 trayMenu = new QMenu;
8792 previewProjector = new QMenu(QTStr("PreviewProjector"));
8793 studioProgramProjector = new QMenu(QTStr("StudioProgramProjector"));
8794 AddProjectorMenuMonitors(previewProjector, this,
8795 SLOT(OpenPreviewProjector()));
8796 AddProjectorMenuMonitors(studioProgramProjector, this,
8797 SLOT(OpenStudioProgramProjector()));
8798 trayMenu->addAction(showHide);
8799 trayMenu->addMenu(previewProjector);
8800 trayMenu->addMenu(studioProgramProjector);
8801 trayMenu->addAction(sysTrayStream);
8802 trayMenu->addAction(sysTrayRecord);
8803 trayMenu->addAction(sysTrayReplayBuffer);
8804 trayMenu->addAction(sysTrayVirtualCam);
8805 trayMenu->addAction(exit);
8806 trayIcon->setContextMenu(trayMenu);
8807 trayIcon->show();
8808
8809 if (outputHandler && !outputHandler->replayBuffer)
8810 sysTrayReplayBuffer->setEnabled(false);
8811
8812 sysTrayVirtualCam->setEnabled(vcamEnabled);
8813
8814 connect(trayIcon.data(),
8815 SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this,
8816 SLOT(IconActivated(QSystemTrayIcon::ActivationReason)));
8817 connect(showHide, SIGNAL(triggered()), this, SLOT(ToggleShowHide()));
8818 connect(sysTrayStream, SIGNAL(triggered()), this,
8819 SLOT(on_streamButton_clicked()));
8820 connect(sysTrayRecord, SIGNAL(triggered()), this,
8821 SLOT(on_recordButton_clicked()));
8822 connect(sysTrayReplayBuffer.data(), &QAction::triggered, this,
8823 &OBSBasic::ReplayBufferClicked);
8824 connect(sysTrayVirtualCam.data(), &QAction::triggered, this,
8825 &OBSBasic::VCamButtonClicked);
8826 connect(exit, SIGNAL(triggered()), this, SLOT(close()));
8827 }
8828
8829 void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason)
8830 {
8831 // Refresh projector list
8832 previewProjector->clear();
8833 studioProgramProjector->clear();
8834 AddProjectorMenuMonitors(previewProjector, this,
8835 SLOT(OpenPreviewProjector()));
8836 AddProjectorMenuMonitors(studioProgramProjector, this,
8837 SLOT(OpenStudioProgramProjector()));
8838
8839 #ifdef __APPLE__
8840 UNUSED_PARAMETER(reason);
8841 #else
8842 if (reason == QSystemTrayIcon::Trigger) {
8843 EnablePreviewDisplay(previewEnabled && !isVisible());
8844 ToggleShowHide();
8845 }
8846 #endif
8847 }
8848
8849 void OBSBasic::SysTrayNotify(const QString &text,
8850 QSystemTrayIcon::MessageIcon n)
8851 {
8852 if (trayIcon && trayIcon->isVisible() &&
8853 QSystemTrayIcon::supportsMessages()) {
8854 QSystemTrayIcon::MessageIcon icon =
8855 QSystemTrayIcon::MessageIcon(n);
8856 trayIcon->showMessage("OBS Studio", text, icon, 10000);
8857 }
8858 }
8859
8860 void OBSBasic::SystemTray(bool firstStarted)
8861 {
8862 if (!QSystemTrayIcon::isSystemTrayAvailable())
8863 return;
8864 if (!trayIcon && !firstStarted)
8865 return;
8866
8867 bool sysTrayWhenStarted = config_get_bool(
8868 GetGlobalConfig(), "BasicWindow", "SysTrayWhenStarted");
8869 bool sysTrayEnabled = config_get_bool(GetGlobalConfig(), "BasicWindow",
8870 "SysTrayEnabled");
8871
8872 if (firstStarted)
8873 SystemTrayInit();
8874
8875 if (!sysTrayEnabled) {
8876 trayIcon->hide();
8877 } else {
8878 trayIcon->show();
8879 if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) {
8880 QTimer::singleShot(50, this, SLOT(hide()));
8881 EnablePreviewDisplay(false);
8882 setVisible(false);
8883 #ifdef __APPLE__
8884 EnableOSXDockIcon(false);
8885 #endif
8886 opt_minimize_tray = false;
8887 }
8888 }
8889
8890 if (isVisible())
8891 showHide->setText(QTStr("Basic.SystemTray.Hide"));
8892 else
8893 showHide->setText(QTStr("Basic.SystemTray.Show"));
8894 }
8895
8896 bool OBSBasic::sysTrayMinimizeToTray()
8897 {
8898 return config_get_bool(GetGlobalConfig(), "BasicWindow",
8899 "SysTrayMinimizeToTray");
8900 }
8901
8902 void OBSBasic::on_actionMainUndo_triggered()
8903 {
8904 undo_s.undo();
8905 }
8906
8907 void OBSBasic::on_actionMainRedo_triggered()
8908 {
8909 undo_s.redo();
8910 }
8911
8912 void OBSBasic::on_actionCopySource_triggered()
8913 {
8914 copyStrings.clear();
8915 bool allowPastingDuplicate = true;
8916
8917 for (auto &selectedSource : GetAllSelectedSourceItems()) {
8918 OBSSceneItem item = ui->sources->Get(selectedSource.row());
8919 if (!item)
8920 continue;
8921
8922 on_actionCopyTransform_triggered();
8923
8924 OBSSource source = obs_sceneitem_get_source(item);
8925
8926 copyStrings.push_front(obs_source_get_name(source));
8927
8928 copyVisible = obs_sceneitem_visible(item);
8929
8930 uint32_t output_flags = obs_source_get_output_flags(source);
8931 if (output_flags & OBS_SOURCE_DO_NOT_DUPLICATE)
8932 allowPastingDuplicate = false;
8933 }
8934
8935 ui->actionPasteRef->setEnabled(true);
8936 ui->actionPasteDup->setEnabled(allowPastingDuplicate);
8937 }
8938
8939 void OBSBasic::on_actionPasteRef_triggered()
8940 {
8941 OBSSource scene_source = GetCurrentSceneSource();
8942 OBSData undo_data = BackupScene(scene_source);
8943
8944 undo_s.push_disabled();
8945
8946 for (auto ©String : copyStrings) {
8947 /* do not allow duplicate refs of the same group in the same scene */
8948 OBSScene scene = GetCurrentScene();
8949 if (!!obs_scene_get_group(scene, copyString))
8950 continue;
8951
8952 OBSBasicSourceSelect::SourcePaste(copyString, copyVisible,
8953 false);
8954 on_actionPasteTransform_triggered();
8955 }
8956
8957 undo_s.pop_disabled();
8958
8959 QString action_name = QTStr("Undo.PasteSourceRef");
8960 const char *scene_name = obs_source_get_name(scene_source);
8961
8962 OBSData redo_data = BackupScene(scene_source);
8963 CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data,
8964 redo_data);
8965 }
8966
8967 void OBSBasic::on_actionPasteDup_triggered()
8968 {
8969 OBSSource scene_source = GetCurrentSceneSource();
8970 OBSData undo_data = BackupScene(scene_source);
8971
8972 undo_s.push_disabled();
8973
8974 for (auto ©String : copyStrings) {
8975 OBSBasicSourceSelect::SourcePaste(copyString, copyVisible,
8976 true);
8977 on_actionPasteTransform_triggered();
8978 }
8979
8980 undo_s.pop_disabled();
8981
8982 QString action_name = QTStr("Undo.PasteSource");
8983 const char *scene_name = obs_source_get_name(scene_source);
8984
8985 OBSData redo_data = BackupScene(scene_source);
8986 CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data,
8987 redo_data);
8988 }
8989
8990 void OBSBasic::AudioMixerCopyFilters()
8991 {
8992 QAction *action = reinterpret_cast<QAction *>(sender());
8993 VolControl *vol = action->property("volControl").value<VolControl *>();
8994 obs_source_t *source = vol->GetSource();
8995
8996 copyFiltersString = obs_source_get_name(source);
8997 }
8998
8999 void OBSBasic::AudioMixerPasteFilters()
9000 {
9001 QAction *action = reinterpret_cast<QAction *>(sender());
9002 VolControl *vol = action->property("volControl").value<VolControl *>();
9003 obs_source_t *dstSource = vol->GetSource();
9004
9005 OBSSource source = obs_get_source_by_name(copyFiltersString);
9006 obs_source_release(source);
9007
9008 if (source == dstSource)
9009 return;
9010
9011 obs_source_copy_filters(dstSource, source);
9012 }
9013
9014 void OBSBasic::SceneCopyFilters()
9015 {
9016 copyFiltersString = obs_source_get_name(GetCurrentSceneSource());
9017 }
9018
9019 void OBSBasic::ScenePasteFilters()
9020 {
9021 OBSSource source = obs_get_source_by_name(copyFiltersString);
9022 obs_source_release(source);
9023
9024 OBSSource dstSource = GetCurrentSceneSource();
9025
9026 if (source == dstSource)
9027 return;
9028
9029 obs_source_copy_filters(dstSource, source);
9030 }
9031
9032 void OBSBasic::on_actionCopyFilters_triggered()
9033 {
9034 OBSSceneItem item = GetCurrentSceneItem();
9035
9036 if (!item)
9037 return;
9038
9039 OBSSource source = obs_sceneitem_get_source(item);
9040
9041 copyFiltersString = obs_source_get_name(source);
9042
9043 ui->actionPasteFilters->setEnabled(true);
9044 }
9045
9046 void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text,
9047 obs_source_t *source,
9048 obs_data_array_t *undo_array,
9049 obs_data_array_t *redo_array)
9050 {
9051 auto undo_redo = [this](const std::string &json) {
9052 obs_data_t *data = obs_data_create_from_json(json.c_str());
9053 obs_data_array_t *array = obs_data_get_array(data, "array");
9054 const char *name = obs_data_get_string(data, "name");
9055 obs_source_t *source = obs_get_source_by_name(name);
9056
9057 obs_source_restore_filters(source, array);
9058 obs_source_release(source);
9059
9060 obs_data_array_release(array);
9061 obs_data_release(data);
9062
9063 if (filters)
9064 filters->UpdateSource(source);
9065 };
9066
9067 const char *name = obs_source_get_name(source);
9068
9069 obs_data_t *undo_data = obs_data_create();
9070 obs_data_t *redo_data = obs_data_create();
9071 obs_data_set_array(undo_data, "array", undo_array);
9072 obs_data_set_array(redo_data, "array", redo_array);
9073 obs_data_set_string(undo_data, "name", name);
9074 obs_data_set_string(redo_data, "name", name);
9075
9076 undo_s.add_action(text, undo_redo, undo_redo,
9077 obs_data_get_json(undo_data),
9078 obs_data_get_json(redo_data));
9079
9080 obs_data_release(undo_data);
9081 obs_data_release(redo_data);
9082 }
9083
9084 void OBSBasic::on_actionPasteFilters_triggered()
9085 {
9086 OBSSource source = obs_get_source_by_name(copyFiltersString);
9087 obs_source_release(source);
9088
9089 OBSSceneItem sceneItem = GetCurrentSceneItem();
9090 OBSSource dstSource = obs_sceneitem_get_source(sceneItem);
9091
9092 if (source == dstSource)
9093 return;
9094
9095 obs_data_array_t *undo_array = obs_source_backup_filters(dstSource);
9096 obs_source_copy_filters(dstSource, source);
9097 obs_data_array_t *redo_array = obs_source_backup_filters(dstSource);
9098
9099 const char *srcName = obs_source_get_name(source);
9100 const char *dstName = obs_source_get_name(dstSource);
9101 QString text =
9102 QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName);
9103
9104 CreateFilterPasteUndoRedoAction(text, dstSource, undo_array,
9105 redo_array);
9106
9107 obs_data_array_release(undo_array);
9108 obs_data_array_release(redo_array);
9109 }
9110
9111 static void ConfirmColor(SourceTree *sources, const QColor &color,
9112 QModelIndexList selectedItems)
9113 {
9114 for (int x = 0; x < selectedItems.count(); x++) {
9115 SourceTreeItem *treeItem =
9116 sources->GetItemWidget(selectedItems[x].row());
9117 treeItem->setStyleSheet("background: " +
9118 color.name(QColor::HexArgb));
9119 treeItem->style()->unpolish(treeItem);
9120 treeItem->style()->polish(treeItem);
9121
9122 OBSSceneItem sceneItem = sources->Get(selectedItems[x].row());
9123 obs_data_t *privData =
9124 obs_sceneitem_get_private_settings(sceneItem);
9125 obs_data_set_int(privData, "color-preset", 1);
9126 obs_data_set_string(privData, "color",
9127 QT_TO_UTF8(color.name(QColor::HexArgb)));
9128 obs_data_release(privData);
9129 }
9130 }
9131
9132 void OBSBasic::ColorChange()
9133 {
9134 QModelIndexList selectedItems =
9135 ui->sources->selectionModel()->selectedIndexes();
9136 QAction *action = qobject_cast<QAction *>(sender());
9137 QPushButton *colorButton = qobject_cast<QPushButton *>(sender());
9138
9139 if (selectedItems.count() == 0)
9140 return;
9141
9142 if (colorButton) {
9143 int preset = colorButton->property("bgColor").value<int>();
9144
9145 for (int x = 0; x < selectedItems.count(); x++) {
9146 SourceTreeItem *treeItem = ui->sources->GetItemWidget(
9147 selectedItems[x].row());
9148 treeItem->setStyleSheet("");
9149 treeItem->setProperty("bgColor", preset);
9150 treeItem->style()->unpolish(treeItem);
9151 treeItem->style()->polish(treeItem);
9152
9153 OBSSceneItem sceneItem =
9154 ui->sources->Get(selectedItems[x].row());
9155 obs_data_t *privData =
9156 obs_sceneitem_get_private_settings(sceneItem);
9157 obs_data_set_int(privData, "color-preset", preset + 1);
9158 obs_data_set_string(privData, "color", "");
9159 obs_data_release(privData);
9160 }
9161
9162 for (int i = 1; i < 9; i++) {
9163 stringstream button;
9164 button << "preset" << i;
9165 QPushButton *cButton =
9166 colorButton->parentWidget()
9167 ->findChild<QPushButton *>(
9168 button.str().c_str());
9169 cButton->setStyleSheet("border: 1px solid black");
9170 }
9171
9172 colorButton->setStyleSheet("border: 2px solid black");
9173 } else if (action) {
9174 int preset = action->property("bgColor").value<int>();
9175
9176 if (preset == 1) {
9177 OBSSceneItem curSceneItem = GetCurrentSceneItem();
9178 SourceTreeItem *curTreeItem =
9179 GetItemWidgetFromSceneItem(curSceneItem);
9180 obs_data_t *curPrivData =
9181 obs_sceneitem_get_private_settings(
9182 curSceneItem);
9183
9184 int oldPreset =
9185 obs_data_get_int(curPrivData, "color-preset");
9186 const QString oldSheet = curTreeItem->styleSheet();
9187
9188 auto liveChangeColor = [=](const QColor &color) {
9189 if (color.isValid()) {
9190 curTreeItem->setStyleSheet(
9191 "background: " +
9192 color.name(QColor::HexArgb));
9193 }
9194 };
9195
9196 auto changedColor = [=](const QColor &color) {
9197 if (color.isValid()) {
9198 ConfirmColor(ui->sources, color,
9199 selectedItems);
9200 }
9201 };
9202
9203 auto rejected = [=]() {
9204 if (oldPreset == 1) {
9205 curTreeItem->setStyleSheet(oldSheet);
9206 curTreeItem->setProperty("bgColor", 0);
9207 } else if (oldPreset == 0) {
9208 curTreeItem->setStyleSheet(
9209 "background: none");
9210 curTreeItem->setProperty("bgColor", 0);
9211 } else {
9212 curTreeItem->setStyleSheet("");
9213 curTreeItem->setProperty("bgColor",
9214 oldPreset - 1);
9215 }
9216
9217 curTreeItem->style()->unpolish(curTreeItem);
9218 curTreeItem->style()->polish(curTreeItem);
9219 };
9220
9221 QColorDialog::ColorDialogOptions options =
9222 QColorDialog::ShowAlphaChannel;
9223
9224 const char *oldColor =
9225 obs_data_get_string(curPrivData, "color");
9226 const char *customColor = *oldColor != 0 ? oldColor
9227 : "#55FF0000";
9228 #ifndef _WIN32
9229 options |= QColorDialog::DontUseNativeDialog;
9230 #endif
9231
9232 QColorDialog *colorDialog = new QColorDialog(this);
9233 colorDialog->setOptions(options);
9234 colorDialog->setCurrentColor(QColor(customColor));
9235 connect(colorDialog, &QColorDialog::currentColorChanged,
9236 liveChangeColor);
9237 connect(colorDialog, &QColorDialog::colorSelected,
9238 changedColor);
9239 connect(colorDialog, &QColorDialog::rejected, rejected);
9240 colorDialog->open();
9241
9242 obs_data_release(curPrivData);
9243 } else {
9244 for (int x = 0; x < selectedItems.count(); x++) {
9245 SourceTreeItem *treeItem =
9246 ui->sources->GetItemWidget(
9247 selectedItems[x].row());
9248 treeItem->setStyleSheet("background: none");
9249 treeItem->setProperty("bgColor", preset);
9250 treeItem->style()->unpolish(treeItem);
9251 treeItem->style()->polish(treeItem);
9252
9253 OBSSceneItem sceneItem = ui->sources->Get(
9254 selectedItems[x].row());
9255 obs_data_t *privData =
9256 obs_sceneitem_get_private_settings(
9257 sceneItem);
9258 obs_data_set_int(privData, "color-preset",
9259 preset);
9260 obs_data_set_string(privData, "color", "");
9261 obs_data_release(privData);
9262 }
9263 }
9264 }
9265 }
9266
9267 SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem)
9268 {
9269 int i = 0;
9270 SourceTreeItem *treeItem = ui->sources->GetItemWidget(i);
9271 OBSSceneItem item = ui->sources->Get(i);
9272 int64_t id = obs_sceneitem_get_id(sceneItem);
9273 while (treeItem && obs_sceneitem_get_id(item) != id) {
9274 i++;
9275 treeItem = ui->sources->GetItemWidget(i);
9276 item = ui->sources->Get(i);
9277 }
9278 if (treeItem)
9279 return treeItem;
9280
9281 return nullptr;
9282 }
9283
9284 void OBSBasic::on_autoConfigure_triggered()
9285 {
9286 AutoConfig test(this);
9287 test.setModal(true);
9288 test.show();
9289 test.exec();
9290 }
9291
9292 void OBSBasic::on_stats_triggered()
9293 {
9294 if (!stats.isNull()) {
9295 stats->show();
9296 stats->raise();
9297 return;
9298 }
9299
9300 OBSBasicStats *statsDlg;
9301 statsDlg = new OBSBasicStats(nullptr);
9302 statsDlg->show();
9303 stats = statsDlg;
9304 }
9305
9306 void OBSBasic::on_actionShowAbout_triggered()
9307 {
9308 if (about)
9309 about->close();
9310
9311 about = new OBSAbout(this);
9312 about->show();
9313
9314 about->setAttribute(Qt::WA_DeleteOnClose, true);
9315 }
9316
9317 void OBSBasic::ResizeOutputSizeOfSource()
9318 {
9319 if (obs_video_active())
9320 return;
9321
9322 QMessageBox resize_output(this);
9323 resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" +
9324 QTStr("ResizeOutputSizeOfSource.Continue"));
9325 QAbstractButton *Yes =
9326 resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole);
9327 resize_output.addButton(QTStr("No"), QMessageBox::NoRole);
9328 resize_output.setIcon(QMessageBox::Warning);
9329 resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource"));
9330 resize_output.exec();
9331
9332 if (resize_output.clickedButton() != Yes)
9333 return;
9334
9335 OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem());
9336
9337 int width = obs_source_get_width(source);
9338 int height = obs_source_get_height(source);
9339
9340 config_set_uint(basicConfig, "Video", "BaseCX", width);
9341 config_set_uint(basicConfig, "Video", "BaseCY", height);
9342 config_set_uint(basicConfig, "Video", "OutputCX", width);
9343 config_set_uint(basicConfig, "Video", "OutputCY", height);
9344
9345 ResetVideo();
9346 ResetOutputs();
9347 config_save_safe(basicConfig, "tmp", nullptr);
9348 on_actionFitToScreen_triggered();
9349 }
9350
9351 QAction *OBSBasic::AddDockWidget(QDockWidget *dock)
9352 {
9353 QAction *action = ui->viewMenuDocks->addAction(dock->windowTitle());
9354 action->setCheckable(true);
9355 assignDockToggle(dock, action);
9356 extraDocks.push_back(dock);
9357
9358 bool lock = ui->lockUI->isChecked();
9359 QDockWidget::DockWidgetFeatures features =
9360 lock ? QDockWidget::NoDockWidgetFeatures
9361 : (QDockWidget::DockWidgetClosable |
9362 QDockWidget::DockWidgetMovable |
9363 QDockWidget::DockWidgetFloatable);
9364
9365 dock->setFeatures(features);
9366
9367 /* prune deleted docks */
9368 for (int i = extraDocks.size() - 1; i >= 0; i--) {
9369 if (!extraDocks[i]) {
9370 extraDocks.removeAt(i);
9371 }
9372 }
9373
9374 return action;
9375 }
9376
9377 OBSBasic *OBSBasic::Get()
9378 {
9379 return reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
9380 }
9381
9382 bool OBSBasic::StreamingActive()
9383 {
9384 if (!outputHandler)
9385 return false;
9386 return outputHandler->StreamingActive();
9387 }
9388
9389 bool OBSBasic::RecordingActive()
9390 {
9391 if (!outputHandler)
9392 return false;
9393 return outputHandler->RecordingActive();
9394 }
9395
9396 bool OBSBasic::ReplayBufferActive()
9397 {
9398 if (!outputHandler)
9399 return false;
9400 return outputHandler->ReplayBufferActive();
9401 }
9402
9403 SceneRenameDelegate::SceneRenameDelegate(QObject *parent)
9404 : QStyledItemDelegate(parent)
9405 {
9406 }
9407
9408 void SceneRenameDelegate::setEditorData(QWidget *editor,
9409 const QModelIndex &index) const
9410 {
9411 QStyledItemDelegate::setEditorData(editor, index);
9412 QLineEdit *lineEdit = qobject_cast<QLineEdit *>(editor);
9413 if (lineEdit)
9414 lineEdit->selectAll();
9415 }
9416
9417 bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event)
9418 {
9419 if (event->type() == QEvent::KeyPress) {
9420 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
9421 if (keyEvent->key() == Qt::Key_Escape) {
9422 QLineEdit *lineEdit = qobject_cast<QLineEdit *>(editor);
9423 if (lineEdit)
9424 lineEdit->undo();
9425 }
9426 }
9427
9428 return QStyledItemDelegate::eventFilter(editor, event);
9429 }
9430
9431 void OBSBasic::UpdatePatronJson(const QString &text, const QString &error)
9432 {
9433 if (!error.isEmpty())
9434 return;
9435
9436 patronJson = QT_TO_UTF8(text);
9437 }
9438
9439 void OBSBasic::PauseRecording()
9440 {
9441 if (!pause || !outputHandler || !outputHandler->fileOutput ||
9442 os_atomic_load_bool(&recording_paused))
9443 return;
9444
9445 obs_output_t *output = outputHandler->fileOutput;
9446
9447 if (obs_output_pause(output, true)) {
9448 pause->setAccessibleName(QTStr("Basic.Main.UnpauseRecording"));
9449 pause->setToolTip(QTStr("Basic.Main.UnpauseRecording"));
9450 pause->blockSignals(true);
9451 pause->setChecked(true);
9452 pause->blockSignals(false);
9453
9454 ui->statusbar->RecordingPaused();
9455
9456 if (trayIcon && trayIcon->isVisible()) {
9457 #ifdef __APPLE__
9458 QIcon trayIconFile =
9459 QIcon(":/res/images/obs_paused_macos.png");
9460 trayIconFile.setIsMask(true);
9461 #else
9462 QIcon trayIconFile =
9463 QIcon(":/res/images/obs_paused.png");
9464 #endif
9465 trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused",
9466 trayIconFile));
9467 }
9468
9469 os_atomic_set_bool(&recording_paused, true);
9470
9471 if (api)
9472 api->on_event(OBS_FRONTEND_EVENT_RECORDING_PAUSED);
9473
9474 if (os_atomic_load_bool(&replaybuf_active))
9475 ShowReplayBufferPauseWarning();
9476 }
9477 }
9478
9479 void OBSBasic::UnpauseRecording()
9480 {
9481 if (!pause || !outputHandler || !outputHandler->fileOutput ||
9482 !os_atomic_load_bool(&recording_paused))
9483 return;
9484
9485 obs_output_t *output = outputHandler->fileOutput;
9486
9487 if (obs_output_pause(output, false)) {
9488 pause->setAccessibleName(QTStr("Basic.Main.PauseRecording"));
9489 pause->setToolTip(QTStr("Basic.Main.PauseRecording"));
9490 pause->blockSignals(true);
9491 pause->setChecked(false);
9492 pause->blockSignals(false);
9493
9494 ui->statusbar->RecordingUnpaused();
9495
9496 if (trayIcon && trayIcon->isVisible()) {
9497 #ifdef __APPLE__
9498 QIcon trayIconFile =
9499 QIcon(":/res/images/tray_active_macos.png");
9500 trayIconFile.setIsMask(true);
9501 #else
9502 QIcon trayIconFile =
9503 QIcon(":/res/images/tray_active.png");
9504 #endif
9505 trayIcon->setIcon(QIcon::fromTheme("obs-tray-active",
9506 trayIconFile));
9507 }
9508
9509 os_atomic_set_bool(&recording_paused, false);
9510
9511 if (api)
9512 api->on_event(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED);
9513 }
9514 }
9515
9516 void OBSBasic::PauseToggled()
9517 {
9518 if (!pause || !outputHandler || !outputHandler->fileOutput)
9519 return;
9520
9521 obs_output_t *output = outputHandler->fileOutput;
9522 bool enable = !obs_output_paused(output);
9523
9524 if (enable)
9525 PauseRecording();
9526 else
9527 UnpauseRecording();
9528 }
9529
9530 void OBSBasic::UpdatePause(bool activate)
9531 {
9532 if (!activate || !outputHandler || !outputHandler->RecordingActive()) {
9533 pause.reset();
9534 return;
9535 }
9536
9537 const char *mode = config_get_string(basicConfig, "Output", "Mode");
9538 bool adv = astrcmpi(mode, "Advanced") == 0;
9539 bool shared;
9540
9541 if (adv) {
9542 const char *recType =
9543 config_get_string(basicConfig, "AdvOut", "RecType");
9544
9545 if (astrcmpi(recType, "FFmpeg") == 0) {
9546 shared = config_get_bool(basicConfig, "AdvOut",
9547 "FFOutputToFile");
9548 } else {
9549 const char *recordEncoder = config_get_string(
9550 basicConfig, "AdvOut", "RecEncoder");
9551 shared = astrcmpi(recordEncoder, "none") == 0;
9552 }
9553 } else {
9554 const char *quality = config_get_string(
9555 basicConfig, "SimpleOutput", "RecQuality");
9556 shared = strcmp(quality, "Stream") == 0;
9557 }
9558
9559 if (!shared) {
9560 pause.reset(new QPushButton());
9561 pause->setAccessibleName(QTStr("Basic.Main.PauseRecording"));
9562 pause->setToolTip(QTStr("Basic.Main.PauseRecording"));
9563 pause->setCheckable(true);
9564 pause->setChecked(false);
9565 pause->setProperty("themeID",
9566 QVariant(QStringLiteral("pauseIconSmall")));
9567
9568 QSizePolicy sp;
9569 sp.setHeightForWidth(true);
9570 pause->setSizePolicy(sp);
9571
9572 connect(pause.data(), &QAbstractButton::clicked, this,
9573 &OBSBasic::PauseToggled);
9574 ui->recordingLayout->addWidget(pause.data());
9575 } else {
9576 pause.reset();
9577 }
9578 }
9579
9580 void OBSBasic::UpdateReplayBuffer(bool activate)
9581 {
9582 if (!activate || !outputHandler ||
9583 !outputHandler->ReplayBufferActive()) {
9584 replay.reset();
9585 return;
9586 }
9587
9588 replay.reset(new QPushButton());
9589 replay->setAccessibleName(QTStr("Basic.Main.SaveReplay"));
9590 replay->setToolTip(QTStr("Basic.Main.SaveReplay"));
9591 replay->setChecked(false);
9592 replay->setProperty("themeID",
9593 QVariant(QStringLiteral("replayIconSmall")));
9594
9595 QSizePolicy sp;
9596 sp.setHeightForWidth(true);
9597 replay->setSizePolicy(sp);
9598
9599 connect(replay.data(), &QAbstractButton::clicked, this,
9600 &OBSBasic::ReplayBufferSave);
9601 replayLayout->addWidget(replay.data());
9602 setTabOrder(replayLayout->itemAt(0)->widget(),
9603 replayLayout->itemAt(1)->widget());
9604 setTabOrder(replayLayout->itemAt(1)->widget(),
9605 ui->buttonsVLayout->itemAt(3)->widget());
9606 }
9607
9608 #define MBYTE (1024ULL * 1024ULL)
9609 #define MBYTES_LEFT_STOP_REC 50ULL
9610 #define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE)
9611
9612 const char *OBSBasic::GetCurrentOutputPath()
9613 {
9614 const char *path = nullptr;
9615 const char *mode = config_get_string(Config(), "Output", "Mode");
9616
9617 if (strcmp(mode, "Advanced") == 0) {
9618 const char *advanced_mode =
9619 config_get_string(Config(), "AdvOut", "RecType");
9620
9621 if (strcmp(advanced_mode, "FFmpeg") == 0) {
9622 path = config_get_string(Config(), "AdvOut",
9623 "FFFilePath");
9624 } else {
9625 path = config_get_string(Config(), "AdvOut",
9626 "RecFilePath");
9627 }
9628 } else {
9629 path = config_get_string(Config(), "SimpleOutput", "FilePath");
9630 }
9631
9632 return path;
9633 }
9634
9635 void OBSBasic::OutputPathInvalidMessage()
9636 {
9637 blog(LOG_ERROR, "Recording stopped because of bad output path");
9638
9639 OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"),
9640 QTStr("Output.BadPath.Text"));
9641 }
9642
9643 bool OBSBasic::OutputPathValid()
9644 {
9645 const char *mode = config_get_string(Config(), "Output", "Mode");
9646 if (strcmp(mode, "Advanced") == 0) {
9647 const char *advanced_mode =
9648 config_get_string(Config(), "AdvOut", "RecType");
9649 if (strcmp(advanced_mode, "FFmpeg") == 0) {
9650 bool is_local = config_get_bool(Config(), "AdvOut",
9651 "FFOutputToFile");
9652 if (!is_local)
9653 return true;
9654 }
9655 }
9656
9657 const char *path = GetCurrentOutputPath();
9658 return path && *path && QDir(path).exists();
9659 }
9660
9661 void OBSBasic::DiskSpaceMessage()
9662 {
9663 blog(LOG_ERROR, "Recording stopped because of low disk space");
9664
9665 OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"),
9666 QTStr("Output.RecordNoSpace.Msg"));
9667 }
9668
9669 bool OBSBasic::LowDiskSpace()
9670 {
9671 const char *path;
9672
9673 path = GetCurrentOutputPath();
9674 if (!path)
9675 return false;
9676
9677 uint64_t num_bytes = os_get_free_disk_space(path);
9678
9679 if (num_bytes < (MAX_BYTES_LEFT))
9680 return true;
9681 else
9682 return false;
9683 }
9684
9685 void OBSBasic::CheckDiskSpaceRemaining()
9686 {
9687 if (LowDiskSpace()) {
9688 StopRecording();
9689 StopReplayBuffer();
9690
9691 DiskSpaceMessage();
9692 }
9693 }
9694
9695 void OBSBasic::ScenesReordered()
9696 {
9697 OBSProjector::UpdateMultiviewProjectors();
9698 }
9699
9700 void OBSBasic::ResetStatsHotkey()
9701 {
9702 QList<OBSBasicStats *> list = findChildren<OBSBasicStats *>();
9703
9704 foreach(OBSBasicStats * s, list) s->Reset();
9705 }
9706
9707 void OBSBasic::on_customContextMenuRequested(const QPoint &pos)
9708 {
9709 QWidget *widget = childAt(pos);
9710 const char *className = nullptr;
9711 if (widget != nullptr)
9712 className = widget->metaObject()->className();
9713
9714 if (!className || strstr(className, "Dock") != nullptr)
9715 ui->viewMenuDocks->exec(mapToGlobal(pos));
9716 }
9717
9718 void OBSBasic::UpdateProjectorHideCursor()
9719 {
9720 for (size_t i = 0; i < projectors.size(); i++)
9721 projectors[i]->SetHideCursor();
9722 }
9723
9724 void OBSBasic::UpdateProjectorAlwaysOnTop(bool top)
9725 {
9726 for (size_t i = 0; i < projectors.size(); i++)
9727 SetAlwaysOnTop(projectors[i], top);
9728 }
9729
9730 void OBSBasic::ResetProjectors()
9731 {
9732 obs_data_array_t *savedProjectorList = SaveProjectors();
9733 ClearProjectors();
9734 LoadSavedProjectors(savedProjectorList);
9735 OpenSavedProjectors();
9736 obs_data_array_release(savedProjectorList);
9737 }
9738
9739 void OBSBasic::on_sourcePropertiesButton_clicked()
9740 {
9741 on_actionSourceProperties_triggered();
9742 }
9743
9744 void OBSBasic::on_sourceFiltersButton_clicked()
9745 {
9746 OpenFilters();
9747 }
9748
9749 void OBSBasic::on_sourceInteractButton_clicked()
9750 {
9751 on_actionInteract_triggered();
9752 }
9753
9754 void OBSBasic::ShowStatusBarMessage(const QString &message)
9755 {
9756 ui->statusbar->clearMessage();
9757 ui->statusbar->showMessage(message, 10000);
9758 }
9759
9760 void OBSBasic::UpdatePreviewSafeAreas()
9761 {
9762 drawSafeAreas = config_get_bool(App()->GlobalConfig(), "BasicWindow",
9763 "ShowSafeAreas");
9764 }
9765