vulcast-vertical/vertical-canvas.cpp
Vulcast 184d132b59
Some checks are pending
Build Windows DLL / build-windows (push) Waiting to run
Rebrand from Aitum Vertical to Vulcast Vertical
- Changed CANVAS_NAME to 'Vulcast Vertical'
- Changed OBS_MODULE_AUTHOR to 'Vulcast'
- Replaced all Aitum UI branding (logo, buttons, links)
- Removed api.aitum.tv version check
- Removed aitum-stream-suite/aitum-multistream module checks
- Updated all locale files (9 languages)
- Updated build metadata (buildspec.json, resource.rc.in, CMakeLists.txt)
- Updated resources.qrc prefix from /aitum to /vulcast
- Added GitHub Actions workflow for Windows build
- Added BUILD-WINDOWS.md with build instructions

Licensed under GPL-2.0 (unchanged from upstream).
2026-06-29 23:04:40 +01:00

9071 lines
278 KiB
C++

#include "vertical-canvas.hpp"
#include <list>
#include "version.h"
#include <obs-frontend-api.h>
#include <obs-module.h>
#include <QDesktopServices>
#include <QGuiApplication>
#include <QLineEdit>
#include <QListWidget>
#include <QMainWindow>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QPushButton>
#include <QTimer>
#include <QToolBar>
#include <QWidgetAction>
#include "audio-wrapper-source.h"
#include "config-dialog.hpp"
#include "display-helpers.hpp"
#include "media-io/video-frame.h"
#include "multi-canvas-source.h"
#include "name-dialog.hpp"
#include "obs-websocket-api.h"
#include "scenes-dock.hpp"
#include "sources-dock.hpp"
#include "transitions-dock.hpp"
#include "util/config-file.h"
#include "util/dstr.h"
#include "util/platform.h"
#include "util/util.hpp"
extern "C" {
#include "file-updater.h"
}
#ifndef _WIN32
#include <dlfcn.h>
#endif
OBS_DECLARE_MODULE()
OBS_MODULE_AUTHOR("Vulcast");
OBS_MODULE_USE_DEFAULT_LOCALE("vertical-canvas", "en-US")
#define HANDLE_RADIUS 4.0f
#define HANDLE_SEL_RADIUS (HANDLE_RADIUS * 1.5f)
#define HELPER_ROT_BREAKPONT 45.0f
#define SPACER_LABEL_MARGIN 6.0f
#define CANVAS_NAME "Vulcast Vertical"
inline std::list<CanvasDock *> canvas_docks;
void clear_canvas_docks()
{
for (const auto &it : canvas_docks) {
it->ClearScenes();
it->StopOutputs();
it->close();
it->deleteLater();
}
canvas_docks.clear();
}
static void ensure_directory(char *path)
{
#ifdef _WIN32
char *backslash = strrchr(path, '\\');
if (backslash) {
*backslash = '/';
}
#endif
char *slash = strrchr(path, '/');
if (slash) {
*slash = 0;
os_mkdirs(path);
*slash = '/';
}
#ifdef _WIN32
if (backslash) {
*backslash = '\\';
}
#endif
}
static void save_canvas()
{
if (canvas_docks.empty()) {
return;
}
char *path = obs_module_config_path("config.json");
if (!path) {
return;
}
ensure_directory(path);
obs_data_t *config = obs_data_create_from_json_file_safe(path, "bak");
if (!config) {
config = obs_data_create();
blog(LOG_INFO, "[Vertical Canvas] New configuration file");
}
const auto canvas = obs_data_array_create();
for (const auto &it : canvas_docks) {
obs_data_t *s = it->SaveSettings();
obs_data_array_push_back(canvas, s);
obs_data_release(s);
}
obs_data_set_array(config, "canvas", canvas);
obs_data_array_release(canvas);
if (obs_data_save_json_safe(config, path, "tmp", "bak")) {
blog(LOG_INFO, "[Vertical Canvas] Saved settings");
} else {
blog(LOG_ERROR, "[Vertical Canvas] Failed saving settings");
}
obs_data_release(config);
bfree(path);
}
void transition_start(void *, calldata_t *)
{
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "MainSceneChanged", Qt::QueuedConnection);
}
}
void frontend_event(obs_frontend_event event, void *private_data)
{
UNUSED_PARAMETER(private_data);
if (event == OBS_FRONTEND_EVENT_EXIT || event == OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN) {
save_canvas();
clear_canvas_docks();
} else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING) {
for (const auto &it : canvas_docks) {
it->StartChangingSceneCollection();
}
} else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP) {
for (const auto &it : canvas_docks) {
it->ClearScenes();
}
} else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED) {
struct obs_frontend_source_list transitions = {};
obs_frontend_get_transitions(&transitions);
for (size_t i = 0; i < transitions.sources.num; i++) {
auto sh = obs_source_get_signal_handler(transitions.sources.array[i]);
signal_handler_connect(sh, "transition_start", transition_start, nullptr);
}
obs_frontend_source_list_free(&transitions);
for (const auto &it : canvas_docks) {
it->LoadScenes();
it->LogScenes();
}
} else if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) {
struct obs_frontend_source_list transitions = {};
obs_frontend_get_transitions(&transitions);
for (size_t i = 0; i < transitions.sources.num; i++) {
auto sh = obs_source_get_signal_handler(transitions.sources.array[i]);
signal_handler_connect(sh, "transition_start", transition_start, nullptr);
}
obs_frontend_source_list_free(&transitions);
for (const auto &it : canvas_docks) {
it->LoadScenes();
it->LogScenes();
it->FinishLoading();
}
if (!canvas_docks.empty()) {
auto cd = canvas_docks.front();
cd->AskUpdate();
}
} else if (event == OBS_FRONTEND_EVENT_SCENE_CHANGED) {
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "MainSceneChanged", Qt::QueuedConnection);
}
} else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTED) {
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "MainStreamStart", Qt::QueuedConnection);
}
} else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPING || event == OBS_FRONTEND_EVENT_STREAMING_STOPPED) {
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "MainStreamStop", Qt::QueuedConnection);
QTimer::singleShot(200, it,
[it] { QMetaObject::invokeMethod(it, "MainStreamStop", Qt::QueuedConnection); });
}
} else if (event == OBS_FRONTEND_EVENT_RECORDING_STARTED) {
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "MainRecordStart", Qt::QueuedConnection);
}
} else if (event == OBS_FRONTEND_EVENT_RECORDING_STOPPING || event == OBS_FRONTEND_EVENT_RECORDING_STOPPED) {
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "MainRecordStop", Qt::QueuedConnection);
QTimer::singleShot(200, it,
[it] { QMetaObject::invokeMethod(it, "MainRecordStop", Qt::QueuedConnection); });
}
} else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED) {
for (const auto &it : canvas_docks) {
QTimer::singleShot(250, it,
[it] { QMetaObject::invokeMethod(it, "MainReplayBufferStart", Qt::QueuedConnection); });
}
} else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING || event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED) {
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "MainReplayBufferStop", Qt::QueuedConnection);
QTimer::singleShot(200, it,
[it] { QMetaObject::invokeMethod(it, "MainReplayBufferStop", Qt::QueuedConnection); });
}
} else if (event == OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED) {
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "MainVirtualCamStart", Qt::QueuedConnection);
}
} else if (event == OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED) {
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "MainVirtualCamStop", Qt::QueuedConnection);
QTimer::singleShot(200, it,
[it] { QMetaObject::invokeMethod(it, "MainVirtualCamStop", Qt::QueuedConnection); });
}
} else if (event == OBS_FRONTEND_EVENT_PROFILE_CHANGED) {
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "ProfileChanged", Qt::QueuedConnection);
}
}
}
static void get_video(void *data, calldata_t *cd)
{
UNUSED_PARAMETER(data);
const auto width = calldata_int(cd, "width");
const auto height = calldata_int(cd, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
calldata_set_ptr(cd, "video", it->GetVideo());
return;
}
}
static void get_stream_settings(void *data, calldata_t *cd)
{
UNUSED_PARAMETER(data);
const auto width = calldata_int(cd, "width");
const auto height = calldata_int(cd, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
it->DisableStreamSettings();
calldata_set_ptr(cd, "outputs", it->SaveStreamOutputs());
return;
}
}
static void set_stream_settings(void *data, calldata_t *cd)
{
UNUSED_PARAMETER(data);
const auto width = calldata_int(cd, "width");
const auto height = calldata_int(cd, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
obs_data_array_t *outputs = (obs_data_array_t *)calldata_ptr(cd, "outputs");
if (outputs) {
it->DisableStreamSettings();
it->LoadStreamOutputs(outputs);
it->UpdateMulti();
}
return;
}
}
static void start_stream_output(void *data, calldata_t *cd)
{
UNUSED_PARAMETER(data);
const auto width = calldata_int(cd, "width");
const auto height = calldata_int(cd, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
auto name = calldata_string(cd, "name");
it->StartStreamOutput(name);
return;
}
}
static void stop_stream_output(void *data, calldata_t *cd)
{
UNUSED_PARAMETER(data);
const auto width = calldata_int(cd, "width");
const auto height = calldata_int(cd, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
auto name = calldata_string(cd, "name");
it->StopStreamOutput(name);
return;
}
}
static void get_stream_output(void *data, calldata_t *cd)
{
UNUSED_PARAMETER(data);
const auto width = calldata_int(cd, "width");
const auto height = calldata_int(cd, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
auto name = calldata_string(cd, "name");
calldata_set_ptr(cd, "output", it->GetStreamOutput(name));
return;
}
}
static void add_chapter(void *data, calldata_t *cd)
{
UNUSED_PARAMETER(data);
const auto width = calldata_int(cd, "width");
const auto height = calldata_int(cd, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
auto output = it->GetRecordOutput();
if (!output) {
continue;
}
proc_handler_t *ph = obs_output_get_proc_handler(output);
calldata cd2;
calldata_init(&cd2);
calldata_set_string(&cd2, "chapter_name", calldata_string(cd, "chapter_name"));
proc_handler_call(ph, "add_chapter", &cd2);
calldata_free(&cd2);
obs_output_release(output);
return;
}
}
static void get_scene(void *data, calldata_t *cd)
{
UNUSED_PARAMETER(data);
const auto width = calldata_int(cd, "width");
const auto height = calldata_int(cd, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
calldata_set_string(cd, "scene", it->GetScene().toUtf8().constData());
return;
}
}
static void switch_scene(void *data, calldata_t *cd)
{
UNUSED_PARAMETER(data);
const auto width = calldata_int(cd, "width");
const auto height = calldata_int(cd, "height");
QString scene = QString::fromUtf8(calldata_string(cd, "scene"));
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
QMetaObject::invokeMethod(it, "SwitchScene", Q_ARG(QString, scene));
}
}
obs_websocket_vendor vendor = nullptr;
void vendor_request_version(obs_data_t *request_data, obs_data_t *response_data, void *)
{
UNUSED_PARAMETER(request_data);
obs_data_set_string(response_data, "version", PROJECT_VERSION);
obs_data_set_bool(response_data, "success", true);
}
void vendor_request_switch_scene(obs_data_t *request_data, obs_data_t *response_data, void *)
{
const char *scene_name = obs_data_get_string(request_data, "scene");
if (!scene_name || !strlen(scene_name)) {
obs_data_set_string(response_data, "error", "'scene' not set");
obs_data_set_bool(response_data, "success", false);
return;
}
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "SwitchScene", Q_ARG(QString, QString::fromUtf8(scene_name)));
}
obs_data_set_bool(response_data, "success", true);
}
void vendor_request_current_scene(obs_data_t *request_data, obs_data_t *response_data, void *)
{
const auto width = obs_data_get_int(request_data, "width");
const auto height = obs_data_get_int(request_data, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
auto scene = obs_scene_get_source(it->GetCurrentScene());
if (scene) {
obs_data_set_string(response_data, "scene", obs_source_get_name(scene));
} else {
obs_data_set_string(response_data, "scene", "");
}
obs_data_set_bool(response_data, "success", true);
return;
}
obs_data_set_bool(response_data, "success", false);
}
void vendor_request_get_scenes(obs_data_t *request_data, obs_data_t *response_data, void *)
{
const auto width = obs_data_get_int(request_data, "width");
const auto height = obs_data_get_int(request_data, "height");
auto sa = obs_data_array_create();
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
auto scenes = it->GetScenes();
for (auto &scene : scenes) {
auto s = obs_data_create();
obs_data_set_string(s, "name", scene.toUtf8().constData());
obs_data_array_push_back(sa, s);
obs_data_release(s);
}
}
obs_data_set_array(response_data, "scenes", sa);
obs_data_array_release(sa);
obs_data_set_bool(response_data, "success", true);
}
void vendor_request_status(obs_data_t *request_data, obs_data_t *response_data, void *)
{
const auto width = obs_data_get_int(request_data, "width");
const auto height = obs_data_get_int(request_data, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
obs_data_set_bool(response_data, "streaming", it->StreamingActive());
obs_data_set_bool(response_data, "recording", it->RecordingActive());
obs_data_set_bool(response_data, "backtrack", it->BacktrackActive());
obs_data_set_bool(response_data, "virtual_camera", it->VirtualCameraActive());
obs_data_set_bool(response_data, "success", true);
return;
}
obs_data_set_bool(response_data, "success", false);
}
void vendor_request_invoke(obs_data_t *request_data, obs_data_t *response_data, void *p)
{
const char *method = static_cast<char *>(p);
const auto width = obs_data_get_int(request_data, "width");
const auto height = obs_data_get_int(request_data, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
QMetaObject::invokeMethod(it, method);
obs_data_set_bool(response_data, "success", true);
return;
}
obs_data_set_bool(response_data, "success", false);
}
void vendor_request_save_replay(obs_data_t *request_data, obs_data_t *response_data, void *p)
{
UNUSED_PARAMETER(p);
const auto width = obs_data_get_int(request_data, "width");
const auto height = obs_data_get_int(request_data, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
QMetaObject::invokeMethod(it, "ReplayButtonClicked",
Q_ARG(QString, QString::fromUtf8(obs_data_get_string(request_data, "filename"))));
obs_data_set_bool(response_data, "success", true);
return;
}
obs_data_set_bool(response_data, "success", false);
}
void vendor_request_update_stream_key(obs_data_t *request_data, obs_data_t *response_data, void *)
{
// Parse request_data to get the new stream_key
const char *new_stream_key = obs_data_get_string(request_data, "stream_key");
const auto width = obs_data_get_int(request_data, "width");
const auto height = obs_data_get_int(request_data, "height");
if (!new_stream_key || !strlen(new_stream_key)) {
obs_data_set_string(response_data, "error", "'stream_key' not set");
obs_data_set_bool(response_data, "success", false);
return;
}
// Loop through each CanvasDock to find the right one
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
// Update stream_key using the UpdateStreamKey method of CanvasDock
QMetaObject::invokeMethod(it, "updateStreamKey", Q_ARG(QString, QString::fromUtf8(new_stream_key)),
Q_ARG(int, (int)obs_data_get_int(request_data, "index")));
obs_data_set_bool(response_data, "success", true);
return;
}
obs_data_set_bool(response_data, "success", false);
}
void vendor_request_update_stream_server(obs_data_t *request_data, obs_data_t *response_data, void *)
{
// Parse request_data to get the new stream_server
const char *new_stream_server = obs_data_get_string(request_data, "stream_server");
const auto width = obs_data_get_int(request_data, "width");
const auto height = obs_data_get_int(request_data, "height");
if (!new_stream_server || !strlen(new_stream_server)) {
obs_data_set_string(response_data, "error", "'stream_server' not set");
obs_data_set_bool(response_data, "success", false);
return;
}
// Loop through each CanvasDock to find the right one
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
// Update stream_server using the UpdateStreamServer method of CanvasDock
QMetaObject::invokeMethod(it, "updateStreamServer", Q_ARG(QString, QString::fromUtf8(new_stream_server)),
Q_ARG(int, (int)obs_data_get_int(request_data, "index")));
obs_data_set_bool(response_data, "success", true);
return;
}
obs_data_set_bool(response_data, "success", false);
}
void vendor_request_add_chapter(obs_data_t *request_data, obs_data_t *response_data, void *)
{
const auto width = obs_data_get_int(request_data, "width");
const auto height = obs_data_get_int(request_data, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
auto output = it->GetRecordOutput();
if (!output) {
continue;
}
proc_handler_t *ph = obs_output_get_proc_handler(output);
calldata cd;
calldata_init(&cd);
calldata_set_string(&cd, "chapter_name", obs_data_get_string(request_data, "chapter_name"));
bool result = proc_handler_call(ph, "add_chapter", &cd);
calldata_free(&cd);
obs_output_release(output);
obs_data_set_bool(response_data, "success", result);
return;
}
obs_data_set_bool(response_data, "success", false);
}
void vendor_request_pause_recording(obs_data_t *request_data, obs_data_t *response_data, void *)
{
const auto width = obs_data_get_int(request_data, "width");
const auto height = obs_data_get_int(request_data, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
auto output = it->GetRecordOutput();
if (!output || !obs_output_active(output) || obs_output_paused(output)) {
continue;
}
obs_output_pause(output, true);
obs_output_release(output);
obs_data_set_bool(response_data, "success", true);
return;
}
obs_data_set_bool(response_data, "success", false);
}
void vendor_request_unpause_recording(obs_data_t *request_data, obs_data_t *response_data, void *)
{
const auto width = obs_data_get_int(request_data, "width");
const auto height = obs_data_get_int(request_data, "height");
for (const auto &it : canvas_docks) {
if ((width && it->GetCanvasWidth() != width) || (height && it->GetCanvasHeight() != height)) {
continue;
}
auto output = it->GetRecordOutput();
if (!output || !obs_output_active(output) || !obs_output_paused(output)) {
continue;
}
obs_output_pause(output, false);
obs_output_release(output);
obs_data_set_bool(response_data, "success", true);
return;
}
obs_data_set_bool(response_data, "success", false);
}
update_info_t *version_update_info = nullptr;
bool version_info_downloaded(void *param, struct file_download_data *file)
{
UNUSED_PARAMETER(param);
if (!file || !file->buffer.num) {
return true;
}
for (const auto &it : canvas_docks) {
QMetaObject::invokeMethod(it, "ApiInfo", Q_ARG(QString, QString::fromUtf8((const char *)file->buffer.array)));
}
if (version_update_info) {
update_info_destroy(version_update_info);
version_update_info = nullptr;
}
return true;
}
bool obs_module_load(void)
{
/* Stream suite check removed in Vulcast fork */
blog(LOG_INFO, "[Vertical Canvas] loaded version %s", PROJECT_VERSION);
obs_frontend_add_event_callback(frontend_event, nullptr);
obs_register_source(&audio_wrapper_source);
obs_register_source(&multi_canvas_source);
auto ph = obs_get_proc_handler();
proc_handler_add(ph, "void vulcast_vertical_get_video(in int width, in int height, out ptr video)", get_video, nullptr);
proc_handler_add(ph, "void vulcast_vertical_get_stream_settings(in int width, in int height, out ptr outputs)",
get_stream_settings, nullptr);
proc_handler_add(ph, "void vulcast_vertical_set_stream_settings(in int width, in int height, in ptr outputs)",
set_stream_settings, nullptr);
proc_handler_add(ph, "void vulcast_vertical_get_stream_output(in int width, in int height, in string name, out ptr output)",
get_stream_output, nullptr);
proc_handler_add(ph, "void vulcast_vertical_start_stream_output(in int width, in int height, in string name)",
start_stream_output, nullptr);
proc_handler_add(ph, "void vulcast_vertical_stop_stream_output(in int width, in int height, in string name)",
stop_stream_output, nullptr);
proc_handler_add(ph, "void vulcast_vertical_add_chapter(in int width, in int height, in string chapter_name)", add_chapter,
nullptr);
proc_handler_add(ph, "void vulcast_vertical_get_scene(in int width, in int height, out string scene)", get_scene, nullptr);
proc_handler_add(ph, "void vulcast_vertical_switch_scene(in int width, in int height, in string scene)", switch_scene,
nullptr);
return true;
}
void obs_module_post_load(void)
{
const auto path = obs_module_config_path("config.json");
obs_data_t *config = obs_data_create_from_json_file_safe(path, "bak");
bfree(path);
if (!config) {
config = obs_data_create();
blog(LOG_WARNING, "[Vertical Canvas] No configuration file loaded");
} else {
blog(LOG_INFO, "[Vertical Canvas] Loaded configuration file");
}
const auto main_window = static_cast<QMainWindow *>(obs_frontend_get_main_window());
auto canvas = obs_data_get_array(config, "canvas");
obs_data_release(config);
if (!canvas) {
canvas = obs_data_array_create();
blog(LOG_WARNING, "[Vertical Canvas] no canvas found in configuration");
}
const auto count = obs_data_array_count(canvas);
if (!count) {
const auto canvasDock = new CanvasDock(nullptr, main_window);
const QString title = QString::fromUtf8(obs_module_text("Vertical"));
const auto name = "VerticalCanvasDock";
obs_frontend_add_dock_by_id(name, title.toUtf8().constData(), canvasDock);
canvas_docks.push_back(canvasDock);
obs_data_array_release(canvas);
blog(LOG_INFO, "[Vertical Canvas] New Canvas created");
return;
}
for (size_t i = 0; i < count; i++) {
const auto item = obs_data_array_item(canvas, i);
const auto canvasDock = new CanvasDock(item, main_window);
const QString title = QString::fromUtf8(obs_module_text("Vertical"));
const auto name = "VerticalCanvasDock";
obs_frontend_add_dock_by_id(name, title.toUtf8().constData(), canvasDock);
obs_data_release(item);
canvas_docks.push_back(canvasDock);
}
obs_data_array_release(canvas);
if (!vendor) {
vendor = obs_websocket_register_vendor("vulcast-vertical");
}
if (!vendor) {
return;
}
obs_websocket_vendor_register_request(vendor, "version", vendor_request_version, nullptr);
obs_websocket_vendor_register_request(vendor, "switch_scene", vendor_request_switch_scene, nullptr);
obs_websocket_vendor_register_request(vendor, "current_scene", vendor_request_current_scene, nullptr);
obs_websocket_vendor_register_request(vendor, "get_scenes", vendor_request_get_scenes, nullptr);
obs_websocket_vendor_register_request(vendor, "status", vendor_request_status, nullptr);
obs_websocket_vendor_register_request(vendor, "start_streaming", vendor_request_invoke, (void *)"StartStream");
obs_websocket_vendor_register_request(vendor, "stop_streaming", vendor_request_invoke, (void *)"StopStream");
obs_websocket_vendor_register_request(vendor, "toggle_streaming", vendor_request_invoke, (void *)"StreamButtonClicked");
obs_websocket_vendor_register_request(vendor, "start_recording", vendor_request_invoke, (void *)"StartRecord");
obs_websocket_vendor_register_request(vendor, "stop_recording", vendor_request_invoke, (void *)"StopRecord");
obs_websocket_vendor_register_request(vendor, "toggle_recording", vendor_request_invoke, (void *)"RecordButtonClicked");
obs_websocket_vendor_register_request(vendor, "start_backtrack", vendor_request_invoke, (void *)"StartReplayBuffer");
obs_websocket_vendor_register_request(vendor, "stop_backtrack", vendor_request_invoke, (void *)"StopReplayBuffer");
obs_websocket_vendor_register_request(vendor, "save_backtrack", vendor_request_save_replay, nullptr);
obs_websocket_vendor_register_request(vendor, "start_virtual_camera", vendor_request_invoke, (void *)"StartVirtualCam");
obs_websocket_vendor_register_request(vendor, "stop_virtual_camera", vendor_request_invoke, (void *)"StopVirtualCam");
obs_websocket_vendor_register_request(vendor, "update_stream_key", vendor_request_update_stream_key, nullptr);
obs_websocket_vendor_register_request(vendor, "update_stream_server", vendor_request_update_stream_server, nullptr);
obs_websocket_vendor_register_request(vendor, "add_chapter", vendor_request_add_chapter, nullptr);
obs_websocket_vendor_register_request(vendor, "pause_recording", vendor_request_pause_recording, nullptr);
obs_websocket_vendor_register_request(vendor, "unpause_recording", vendor_request_unpause_recording, nullptr);
/* Version check removed in Vulcast fork — no external API calls */
}
void obs_module_unload(void)
{
if (vendor && obs_get_module("obs-websocket")) {
obs_websocket_vendor_unregister_request(vendor, "version");
obs_websocket_vendor_unregister_request(vendor, "switch_scene");
obs_websocket_vendor_unregister_request(vendor, "current_scene");
obs_websocket_vendor_unregister_request(vendor, "get_scenes");
obs_websocket_vendor_unregister_request(vendor, "status");
obs_websocket_vendor_unregister_request(vendor, "start_streaming");
obs_websocket_vendor_unregister_request(vendor, "stop_streaming");
obs_websocket_vendor_unregister_request(vendor, "toggle_streaming");
obs_websocket_vendor_unregister_request(vendor, "start_recording");
obs_websocket_vendor_unregister_request(vendor, "stop_recording");
obs_websocket_vendor_unregister_request(vendor, "toggle_recording");
obs_websocket_vendor_unregister_request(vendor, "start_backtrack");
obs_websocket_vendor_unregister_request(vendor, "stop_backtrack");
obs_websocket_vendor_unregister_request(vendor, "save_backtrack");
obs_websocket_vendor_unregister_request(vendor, "start_virtual_camera");
obs_websocket_vendor_unregister_request(vendor, "stop_virtual_camera");
obs_websocket_vendor_unregister_request(vendor, "update_stream_key");
obs_websocket_vendor_unregister_request(vendor, "update_stream_server");
}
obs_frontend_remove_event_callback(frontend_event, nullptr);
if (version_update_info) {
update_info_destroy(version_update_info);
version_update_info = nullptr;
}
}
MODULE_EXPORT const char *obs_module_description(void)
{
return obs_module_text("Description");
}
MODULE_EXPORT const char *obs_module_name(void)
{
return obs_module_text("VerticalCanvas");
}
CanvasScenesDock *CanvasDock::GetScenesDock()
{
return scenesDock;
}
void CanvasDock::AddScene(QString duplicate, bool ask_name)
{
std::string name = duplicate.isEmpty() ? obs_module_text("VerticalScene") : duplicate.toUtf8().constData();
obs_source_t *s = obs_canvas_get_source_by_name(canvas, name.c_str());
int i = 0;
while (s) {
obs_source_release(s);
i++;
name = obs_module_text("VerticalScene");
name += " ";
name += std::to_string(i);
s = obs_canvas_get_source_by_name(canvas, name.c_str());
}
do {
obs_source_release(s);
if (ask_name && !NameDialog::AskForName(this, QString::fromUtf8(obs_module_text("SceneName")), name)) {
break;
}
s = obs_canvas_get_source_by_name(canvas, name.c_str());
if (s) {
continue;
}
obs_source_t *new_scene = nullptr;
if (!duplicate.isEmpty()) {
auto origScene = obs_canvas_get_scene_by_name(canvas, duplicate.toUtf8().constData());
if (origScene) {
new_scene = obs_scene_get_source(obs_scene_duplicate(origScene, name.c_str(), OBS_SCENE_DUP_REFS));
obs_scene_release(origScene);
}
}
if (!new_scene) {
obs_scene_t *canvas_scene = obs_canvas_scene_create(canvas, name.c_str());
new_scene = obs_scene_get_source(canvas_scene);
std::string ssn = obs_canvas_get_name(canvas);
ssn += " ";
ssn += obs_frontend_get_locale_string("Basic.Hotkeys.SelectScene");
obs_hotkey_register_source(
new_scene, "OBSBasic.SelectScene", ssn.c_str(),
[](void *data, obs_hotkey_id, obs_hotkey_t *key, bool pressed) {
if (!pressed) {
return;
}
auto p = (CanvasDock *)data;
auto potential_source = (obs_weak_source_t *)obs_hotkey_get_registerer(key);
OBSSourceAutoRelease source = obs_weak_source_get_source(potential_source);
if (source) {
auto sn = QString::fromUtf8(obs_source_get_name(source));
QMetaObject::invokeMethod(p, "SwitchScene", Q_ARG(QString, sn), Q_ARG(bool, true));
}
},
this);
}
auto sh = obs_source_get_signal_handler(new_scene);
signal_handler_connect(sh, "rename", source_rename, this);
auto sn = QString::fromUtf8(obs_source_get_name(new_scene));
if (scenesCombo) {
scenesCombo->addItem(sn);
}
if (scenesDock) {
scenesDock->sceneList->addItem(sn);
}
SwitchScene(sn);
obs_source_release(new_scene);
} while (ask_name && s);
}
void CanvasDock::RemoveScene(const QString &sceneName)
{
auto s = obs_canvas_get_source_by_name(canvas, sceneName.toUtf8().constData());
if (!s) {
return;
}
if (!obs_source_is_scene(s)) {
obs_source_release(s);
return;
}
QMessageBox mb(QMessageBox::Question, QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.Title")),
QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.Text"))
.arg(QString::fromUtf8(obs_source_get_name(s))),
QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No));
mb.setDefaultButton(QMessageBox::NoButton);
if (mb.exec() == QMessageBox::Yes) {
obs_source_remove(s);
}
obs_source_release(s);
}
void CanvasDock::SetLinkedScene(obs_source_t *scene_, const QString &linkedScene)
{
auto ss = obs_source_get_settings(scene_);
auto c = obs_data_get_array(ss, "canvas");
auto count = obs_data_array_count(c);
obs_data_t *found = nullptr;
for (size_t i = 0; i < count; i++) {
auto item = obs_data_array_item(c, i);
if (!item) {
continue;
}
if (obs_data_get_int(item, "width") == canvas_width && obs_data_get_int(item, "height") == canvas_height) {
found = item;
if (linkedScene.isEmpty()) {
obs_data_array_erase(c, i);
}
break;
}
obs_data_release(item);
}
if (!linkedScene.isEmpty()) {
if (!found) {
if (!c) {
c = obs_data_array_create();
obs_data_set_array(ss, "canvas", c);
}
found = obs_data_create();
obs_data_set_int(found, "width", canvas_width);
obs_data_set_int(found, "height", canvas_height);
obs_data_array_push_back(c, found);
}
obs_data_set_string(found, "scene", linkedScene.toUtf8().constData());
}
obs_data_release(ss);
obs_data_release(found);
obs_data_array_release(c);
}
bool CanvasDock::HasScene(QString sceneName) const
{
if (scenesCombo) {
for (int i = 0; i < scenesCombo->count(); i++) {
if (sceneName == scenesCombo->itemText(i)) {
return true;
}
}
}
if (scenesDock) {
for (int i = 0; i < scenesDock->sceneList->count(); i++) {
if (sceneName == scenesDock->sceneList->item(i)->text()) {
return true;
}
}
}
return false;
}
void CanvasDock::CheckReplayBuffer(bool start)
{
if (replayAlwaysOn) {
StartReplayBuffer();
return;
}
if (start && !startReplay) {
return;
}
bool active = obs_frontend_streaming_active() || obs_frontend_recording_active() || obs_frontend_replay_buffer_active() ||
(recordOutput && obs_output_active(recordOutput));
for (auto it = streamOutputs.begin(); !active && it != streamOutputs.end(); ++it) {
active = it->enabled && it->output && !it->stopping && obs_output_active(it->output);
}
if (start && active) {
StartReplayBuffer();
} else if (!active) {
if (!start) {
StopReplayBuffer();
}
if (canvas && obs_canvas_has_video(canvas)) {
DestroyVideo();
}
}
}
void CanvasDock::CreateScenesRow()
{
const auto sceneRow = new QHBoxLayout(this);
scenesCombo = new QComboBox;
connect(scenesCombo, &QComboBox::currentTextChanged, [this]() { SwitchScene(scenesCombo->currentText()); });
sceneRow->addWidget(scenesCombo, 1);
linkedButton = new LockedCheckBox;
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
connect(linkedButton, &QCheckBox::checkStateChanged, [this] {
#else
connect(linkedButton, &QCheckBox::stateChanged, [this] {
#endif
auto current_scene = obs_frontend_get_current_scene();
if (!current_scene) {
return;
}
SetLinkedScene(current_scene, linkedButton->isChecked() ? scenesCombo->currentText() : "");
obs_source_release(current_scene);
});
sceneRow->addWidget(linkedButton);
auto addButton = new QPushButton;
addButton->setProperty("themeID", "addIconSmall");
addButton->setProperty("class", "icon-plus");
addButton->setToolTip(QString::fromUtf8(obs_module_text("AddVerticalScene")));
connect(addButton, &QPushButton::clicked, [this] { AddScene(); });
sceneRow->addWidget(addButton);
auto removeButton = new QPushButton;
removeButton->setProperty("themeID", "removeIconSmall");
removeButton->setProperty("class", "icon-minus");
removeButton->setToolTip(QString::fromUtf8(obs_module_text("RemoveVerticalScene")));
connect(removeButton, &QPushButton::clicked, [this] { RemoveScene(scenesCombo->currentText()); });
sceneRow->addWidget(removeButton);
mainLayout->insertLayout(0, sceneRow);
}
CanvasDock::CanvasDock(obs_data_t *settings, QWidget *parent)
: QFrame(parent),
action(nullptr),
mainLayout(new QVBoxLayout(this)),
preview(new OBSQTDisplay(this)),
eventFilter(BuildEventFilter())
{
if (!settings) {
settings = obs_data_create();
obs_data_set_bool(settings, "backtrack", true);
first_time = true;
}
partnerBlockTime = (time_t)obs_data_get_int(settings, "partner_block");
canvas_width = (uint32_t)obs_data_get_int(settings, "width");
if ((canvas_width & 1) == 1) {
canvas_width++;
}
canvas_height = (uint32_t)obs_data_get_int(settings, "height");
if ((canvas_height & 1) == 1) {
canvas_height++;
}
if (!canvas_width || !canvas_height) {
canvas_width = 1080;
canvas_height = 1920;
}
streamingVideoBitrate = (uint32_t)obs_data_get_int(settings, "streaming_video_bitrate");
if (!streamingVideoBitrate) {
streamingVideoBitrate = (uint32_t)obs_data_get_int(settings, "video_bitrate");
}
streamingMatchMain = obs_data_get_bool(settings, "streaming_match_main");
recordVideoBitrate = (uint32_t)obs_data_get_int(settings, "record_video_bitrate");
if (!recordVideoBitrate) {
recordVideoBitrate = (uint32_t)obs_data_get_int(settings, "video_bitrate");
}
max_size_mb = (uint32_t)obs_data_get_int(settings, "max_size_mb");
max_time_sec = (uint32_t)obs_data_get_int(settings, "max_time_sec");
recordingMatchMain = obs_data_get_bool(settings, "recording_match_main");
audioBitrate = (uint32_t)obs_data_get_int(settings, "audio_bitrate");
startReplay = obs_data_get_bool(settings, "backtrack");
replayAlwaysOn = false;
replayDuration = (uint32_t)obs_data_get_int(settings, "backtrack_seconds");
replayPath = obs_data_get_string(settings, "backtrack_path");
virtual_cam_mode = (uint32_t)obs_data_get_int(settings, "virtual_camera_mode");
auto so = obs_data_get_array(settings, "stream_outputs");
multi_rtmp = LoadStreamOutputs(so);
obs_data_array_release(so);
if (streamOutputs.empty() && strlen(obs_data_get_string(settings, "stream_server"))) {
StreamServer ss;
ss.stream_server = obs_data_get_string(settings, "stream_server");
ss.stream_key = obs_data_get_string(settings, "stream_key");
bool whip = strstr(ss.stream_server.c_str(), "whip") != nullptr;
ss.service = obs_service_create(whip ? "whip_custom" : "rtmp_custom", "vertical_canvas_stream_service_0", nullptr,
nullptr);
streamOutputs.push_back(ss);
}
stream_delay_enabled = obs_data_get_bool(settings, "stream_delay_enabled");
stream_delay_duration = (uint32_t)obs_data_get_int(settings, "stream_delay_duration");
stream_delay_preserve = obs_data_get_bool(settings, "stream_delay_preserve");
stream_advanced_settings = obs_data_get_bool(settings, "stream_advanced_settings");
stream_audio_track = (int)obs_data_get_int(settings, "stream_audio_track");
stream_encoder = obs_data_get_string(settings, "stream_encoder");
stream_encoder_settings = obs_data_get_obj(settings, "stream_encoder_settings");
if (!stream_encoder_settings) {
stream_encoder_settings = obs_data_create();
}
recordPath = obs_data_get_string(settings, "record_path");
record_advanced_settings = obs_data_get_bool(settings, "record_advanced_settings");
filename_formatting = obs_data_get_string(settings, "filename_formatting");
file_format = obs_data_get_string(settings, "file_format");
record_audio_tracks = obs_data_get_int(settings, "record_audio_tracks");
if (!record_audio_tracks) {
record_audio_tracks = 1;
}
record_encoder = obs_data_get_string(settings, "record_encoder");
record_encoder_settings = obs_data_get_obj(settings, "record_encoder_settings");
if (!record_encoder_settings) {
record_encoder_settings = obs_data_create();
}
preview_disabled = obs_data_get_bool(settings, "preview_disabled");
virtual_cam_warned = obs_data_get_bool(settings, "virtual_cam_warned");
if (!record_advanced_settings && (replayAlwaysOn || startReplay)) {
const auto profile_config = obs_frontend_get_profile_config();
if (!config_get_bool(profile_config, "AdvOut", "RecRB") ||
!config_get_bool(profile_config, "SimpleOutput", "RecRB")) {
config_set_bool(profile_config, "AdvOut", "RecRB", true);
config_set_bool(profile_config, "SimpleOutput", "RecRB", true);
config_save(profile_config);
}
}
size_t idx = 0;
const char *id;
while (obs_enum_transition_types(idx++, &id)) {
if (obs_is_source_configurable(id)) {
continue;
}
const char *name = obs_source_get_display_name(id);
OBSSourceAutoRelease tr = obs_source_create_private(id, name, NULL);
transitions.emplace_back(tr);
//signals "transition_stop" and "transition_video_stop"
// TransitionFullyStopped TransitionStopped
}
obs_data_array_t *transition_array = obs_data_get_array(settings, "transitions");
if (transition_array) {
size_t c = obs_data_array_count(transition_array);
for (size_t i = 0; i < c; i++) {
obs_data_t *td = obs_data_array_item(transition_array, i);
if (!td) {
continue;
}
OBSSourceAutoRelease transition = obs_load_private_source(td);
if (transition) {
transitions.emplace_back(transition);
}
obs_data_release(td);
}
obs_data_array_release(transition_array);
}
auto transition = GetTransition(obs_data_get_string(settings, "transition"));
if (!transition) {
transition = GetTransition(obs_source_get_display_name("fade_transition"));
}
SwapTransition(transition);
setObjectName(QStringLiteral("contextContainer"));
setContentsMargins(0, 0, 0, 0);
setLayout(mainLayout);
const QString title = QString::fromUtf8(obs_module_text("Vertical"));
const QString replayName = title + " " + QString::fromUtf8(obs_module_text("Backtrack"));
auto hotkeyData = obs_data_get_obj(settings, "backtrack_hotkeys");
replayOutput = obs_output_create("replay_buffer", replayName.toUtf8().constData(), nullptr, hotkeyData);
obs_data_release(hotkeyData);
auto rpsh = obs_output_get_signal_handler(replayOutput);
signal_handler_connect(rpsh, "saved", replay_saved, this);
if (obs_data_get_bool(settings, "scenes_row")) {
CreateScenesRow();
}
scenesDock = new CanvasScenesDock(this, parent);
scenesDock->SetGridMode(obs_data_get_bool(settings, "grid_mode"));
const auto scenesName = "VerticalCanvasDockScenes";
const auto scenesTitle = title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.Scenes"));
obs_frontend_add_dock_by_id(scenesName, scenesTitle.toUtf8().constData(), scenesDock);
sourcesDock = new CanvasSourcesDock(this, parent);
const auto sourcesName = "VerticalCanvasDockSources";
const auto sourcesTitle = title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.Sources"));
obs_frontend_add_dock_by_id(sourcesName, sourcesTitle.toUtf8().constData(), sourcesDock);
transitionsDock = new CanvasTransitionsDock(this, parent);
const auto transitionsName = "VerticalCanvasDockTransitions";
const auto transitionsTitle = title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.SceneTransitions"));
obs_frontend_add_dock_by_id(transitionsName, transitionsTitle.toUtf8().constData(), transitionsDock);
preview->setObjectName(QStringLiteral("preview"));
preview->setMinimumSize(QSize(24, 24));
QSizePolicy sizePolicy1(QSizePolicy::Expanding, QSizePolicy::Expanding);
sizePolicy1.setHorizontalStretch(0);
sizePolicy1.setVerticalStretch(0);
sizePolicy1.setHeightForWidth(preview->sizePolicy().hasHeightForWidth());
preview->setSizePolicy(sizePolicy1);
preview->setMouseTracking(true);
preview->setFocusPolicy(Qt::StrongFocus);
preview->installEventFilter(eventFilter.get());
auto addDrawCallback = [this]() {
obs_display_add_draw_callback(preview->GetDisplay(), DrawPreview, this);
};
preview->show();
connect(preview, &OBSQTDisplay::DisplayCreated, addDrawCallback);
obs_display_set_enabled(preview->GetDisplay(), !preview_disabled);
auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) {
QAction *nudge = new QAction(preview);
nudge->setShortcut(seq);
nudge->setShortcutContext(Qt::WidgetShortcut);
preview->addAction(nudge);
connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); });
};
addNudge(Qt::Key_Up, MoveDir::Up, 1);
addNudge(Qt::Key_Down, MoveDir::Down, 1);
addNudge(Qt::Key_Left, MoveDir::Left, 1);
addNudge(Qt::Key_Right, MoveDir::Right, 1);
addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10);
addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10);
addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10);
addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10);
QAction *deleteAction = new QAction(preview);
connect(deleteAction, &QAction::triggered, [this]() {
obs_sceneitem_t *sceneItem = GetSelectedItem();
if (!sceneItem) {
return;
}
QMessageBox mb(QMessageBox::Question, QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.Title")),
QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.Text"))
.arg(QString::fromUtf8(obs_source_get_name(obs_sceneitem_get_source(sceneItem)))),
QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No));
mb.setDefaultButton(QMessageBox::NoButton);
if (mb.exec() == QMessageBox::Yes) {
obs_sceneitem_remove(sceneItem);
}
});
#ifdef __APPLE__
deleteAction->setShortcut({Qt::Key_Backspace});
#else
deleteAction->setShortcut({Qt::Key_Delete});
#endif
deleteAction->setShortcutContext(Qt::WidgetShortcut);
preview->addAction(deleteAction);
mainLayout->addWidget(preview, 1);
preview->setVisible(!preview_disabled);
previewDisabledWidget = new QFrame;
auto l = new QVBoxLayout;
auto enablePreviewButton =
new QPushButton(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.PreviewConextMenu.Enable")));
connect(enablePreviewButton, &QPushButton::clicked, [this] {
preview_disabled = false;
obs_display_set_enabled(preview->GetDisplay(), true);
preview->setVisible(true);
previewDisabledWidget->setVisible(false);
});
l->addWidget(enablePreviewButton);
previewDisabledWidget->setLayout(l);
mainLayout->addWidget(previewDisabledWidget, 1);
previewDisabledWidget->setVisible(preview_disabled);
auto buttonRow = new QHBoxLayout(this);
buttonRow->setContentsMargins(0, 0, 0, 0);
auto streamButtonGroup = new QWidget();
auto streamButtonGroupLayout = new QHBoxLayout();
streamButtonGroupLayout->setContentsMargins(0, 0, 0, 0);
streamButtonGroupLayout->setSpacing(0);
streamButtonGroup->setLayout(streamButtonGroupLayout);
streamButton = new QPushButton;
streamButton->setMinimumHeight(30);
streamButton->setObjectName(QStringLiteral("canvasStream"));
streamButton->setIcon(streamInactiveIcon);
streamButton->setCheckable(true);
streamButton->setChecked(false);
streamButton->setToolTip(QString::fromUtf8(obs_module_text("StreamVertical")));
streamButton->setStyleSheet(
QString::fromUtf8("QPushButton:checked{background: rgb(0,210,153);}") +
QString::fromUtf8(multi_rtmp ? "QPushButton{border-top-right-radius: 0; border-bottom-right-radius: 0;}" : ""));
connect(streamButton, SIGNAL(clicked()), this, SLOT(StreamButtonClicked()));
streamButtonGroup->layout()->addWidget(streamButton);
// Little up arrow in the case of there being multiple enabled outputs
streamButtonMulti = new QPushButton;
streamButtonMulti->setMinimumHeight(30);
streamButtonMulti->setObjectName(QStringLiteral("canvasStreamMulti"));
streamButtonMulti->setToolTip(QString::fromUtf8(obs_module_text("StreamVerticalMulti")));
streamButtonMulti->setChecked(false);
auto multi_menu = new QMenu(this);
connect(multi_menu, &QMenu::aboutToShow, [this, multi_menu] { StreamButtonMultiMenu(multi_menu); });
streamButtonMulti->setMenu(multi_menu);
streamButtonMulti->setStyleSheet(
QString::fromUtf8("QPushButton{width: 16px; border-top-left-radius: 0; border-bottom-left-radius: 0;}"));
streamButtonMulti->setVisible(multi_rtmp);
streamButtonGroup->layout()->addWidget(streamButtonMulti);
buttonRow->addWidget(streamButtonGroup);
recordButton = new QPushButton;
recordButton->setMinimumHeight(30);
recordButton->setObjectName(QStringLiteral("canvasRecord"));
recordButton->setIcon(recordInactiveIcon);
recordButton->setCheckable(true);
recordButton->setChecked(false);
recordButton->setStyleSheet(QString::fromUtf8("QPushButton:checked{background: rgb(255,0,0);}"));
recordButton->setToolTip(QString::fromUtf8(obs_module_text("RecordVertical")));
connect(recordButton, SIGNAL(clicked()), this, SLOT(RecordButtonClicked()));
buttonRow->addWidget(recordButton);
auto replayButtonGroupLayout = new QHBoxLayout();
replayButtonGroupLayout->setContentsMargins(0, 0, 0, 0);
replayButtonGroupLayout->setSpacing(0);
replayButton = new QPushButton;
replayButton->setMinimumHeight(30);
replayButton->setObjectName(QStringLiteral("canvasReplay"));
replayButton->setIcon(replayInactiveIcon);
replayButton->setContentsMargins(0, 0, 0, 0);
replayButton->setToolTip(QString::fromUtf8(obs_module_text("BacktrackClipVertical")));
replayButton->setCheckable(true);
replayButton->setStyleSheet(QString::fromUtf8(
"QPushButton:checked{background: rgb(26,87,255);} QPushButton{width: 32px; padding-left: 0px; padding-right: 0px; border-top-left-radius: 0; border-bottom-left-radius: 0;}"));
connect(replayButton, SIGNAL(clicked()), this, SLOT(ReplayButtonClicked()));
replayEnableButton = new QPushButton;
replayEnableButton->setMinimumHeight(30);
replayEnableButton->setObjectName(QStringLiteral("canvasBacktrackEnable"));
replayEnableButton->setToolTip(QString::fromUtf8(obs_module_text("BacktrackOn")));
replayEnableButton->setCheckable(true);
replayEnableButton->setChecked(false);
replayEnableButton->setStyleSheet(QString::fromUtf8(
"QPushButton:checked{background: rgb(26,87,255);} QPushButton{ border-top-right-radius: 0; border-bottom-right-radius: 0; width: 32px; padding-left: 0px; padding-right: 0px;}"));
replayEnable = new QCheckBox;
auto testl = new QHBoxLayout;
replayEnableButton->setLayout(testl);
testl->addWidget(replayEnable);
connect(replayEnableButton, &QPushButton::clicked, [this] {
replayAlwaysOn = !replayAlwaysOn;
replayEnableButton->setChecked(replayAlwaysOn);
replayEnable->setChecked(replayAlwaysOn);
CheckReplayBuffer();
});
backtrack_hotkey = obs_hotkey_pair_register_frontend(
"VerticalCanvasDockStartBacktrack",
(title + " " + QString::fromUtf8(obs_module_text("BacktrackOn"))).toUtf8().constData(),
"VerticalCanvasDockStopBacktrack",
(title + " " + QString::fromUtf8(obs_module_text("BacktrackOff"))).toUtf8().constData(),
[](void *param, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) {
auto cd = (CanvasDock *)param;
if (!cd->replayAlwaysOn && pressed) {
cd->replayAlwaysOn = true;
cd->replayEnableButton->setChecked(cd->replayAlwaysOn);
cd->replayEnable->setChecked(cd->replayAlwaysOn);
cd->CheckReplayBuffer();
return true;
}
return false;
},
[](void *param, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) {
auto cd = (CanvasDock *)param;
if (cd->replayAlwaysOn && pressed) {
cd->replayAlwaysOn = false;
cd->replayEnableButton->setChecked(cd->replayAlwaysOn);
cd->replayEnable->setChecked(cd->replayAlwaysOn);
cd->CheckReplayBuffer();
return true;
}
return false;
},
this, this);
obs_data_array_t *start_hotkey = obs_data_get_array(settings, "start_backtrack_hotkey");
obs_data_array_t *stop_hotkey = obs_data_get_array(settings, "stop_backtrack_hotkey");
obs_hotkey_pair_load(backtrack_hotkey, start_hotkey, stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
connect(replayEnable, &QCheckBox::checkStateChanged, [this] {
#else
connect(replayEnable, &QCheckBox::stateChanged, [this] {
#endif
if (replayEnable->isChecked() != replayEnableButton->isChecked()) {
replayEnableButton->setChecked(replayEnable->isChecked());
if (replayEnable->isChecked() == replayAlwaysOn) {
return;
}
replayAlwaysOn = replayEnable->isChecked();
CheckReplayBuffer();
}
});
replayButtonGroupLayout->addWidget(replayEnableButton);
replayButtonGroupLayout->addWidget(replayButton);
buttonRow->addLayout(replayButtonGroupLayout);
virtualCamButton = new QPushButton;
virtualCamButton->setMinimumHeight(30);
virtualCamButton->setObjectName(QStringLiteral("canvasVirtualCam"));
virtualCamButton->setIcon(virtualCamInactiveIcon);
virtualCamButton->setCheckable(true);
virtualCamButton->setChecked(false);
virtualCamButton->setStyleSheet(QString::fromUtf8("QPushButton:checked{background: rgb(192,128,0);}"));
virtualCamButton->setToolTip(QString::fromUtf8(obs_module_text("VirtualCameraVertical")));
connect(virtualCamButton, SIGNAL(clicked()), this, SLOT(VirtualCamButtonClicked()));
buttonRow->addWidget(virtualCamButton);
statusLabel = new QLabel;
statusLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
buttonRow->addWidget(statusLabel, 1);
recordDurationTimer.setInterval(1000);
recordDurationTimer.setSingleShot(false);
connect(&recordDurationTimer, &QTimer::timeout, [this] {
if (obs_output_active(recordOutput)) {
int totalFrames = obs_output_get_total_frames(recordOutput);
video_t *output_video = obs_output_video(recordOutput);
uint64_t frameTimeNs = video_output_get_frame_time(output_video);
auto t = QTime::fromMSecsSinceStartOfDay((int)util_mul_div64(totalFrames, frameTimeNs, 1000000ULL));
recordButton->setText(t.toString(t.hour() ? "hh:mm:ss" : "mm:ss"));
} else if (!recordButton->text().isEmpty()) {
recordButton->setText("");
}
QString streamButtonText;
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (!obs_output_active(it->output)) {
continue;
}
int totalFrames = obs_output_get_total_frames(it->output);
video_t *output_video = obs_output_video(it->output);
uint64_t frameTimeNs = video_output_get_frame_time(output_video);
auto t = QTime::fromMSecsSinceStartOfDay((int)util_mul_div64(totalFrames, frameTimeNs, 1000000ULL));
streamButtonText = t.toString(t.hour() ? "hh:mm:ss" : "mm:ss");
break;
}
if (streamButton->text() != streamButtonText) {
streamButton->setText(streamButtonText);
}
});
recordDurationTimer.start();
replayStatusResetTimer.setInterval(4000);
replayStatusResetTimer.setSingleShot(true);
connect(&replayStatusResetTimer, &QTimer::timeout, [this] { statusLabel->setText(""); });
configButton = new QPushButton(this);
configButton->setMinimumHeight(30);
configButton->setProperty("themeID", "configIconSmall");
configButton->setProperty("class", "icon-gear");
configButton->setFlat(true);
configButton->setAutoDefault(false);
configButton->setToolTip(QString::fromUtf8(obs_module_text("VerticalSettings")));
connect(configButton, SIGNAL(clicked()), this, SLOT(ConfigButtonClicked()));
buttonRow->addWidget(configButton);
/* Vulcast branding — no external buttons */
setStyleSheet(QString::fromUtf8("QPushButton{padding-left: 4px; padding-right: 4px;}"));
mainLayout->addLayout(buttonRow);
obs_enter_graphics();
gs_render_start(true);
gs_vertex2f(0.0f, 0.0f);
gs_vertex2f(0.0f, 1.0f);
gs_vertex2f(1.0f, 0.0f);
gs_vertex2f(1.0f, 1.0f);
box = gs_render_save();
obs_leave_graphics();
currentSceneName = QString::fromUtf8(obs_data_get_string(settings, "current_scene"));
auto sh = obs_get_signal_handler();
signal_handler_connect(sh, "source_rename", source_rename, this);
signal_handler_connect(sh, "source_remove", source_remove, this);
signal_handler_connect(sh, "source_destroy", source_remove, this);
//signal_handler_connect(sh, "source_create", source_create, this);
//signal_handler_connect(sh, "source_load", source_load, this);
signal_handler_connect(sh, "source_save", source_save, this);
virtual_cam_hotkey = obs_hotkey_pair_register_frontend(
"VerticalCanvasDockStartVirtualCam",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.StartVirtualCam"))).toUtf8().constData(),
"VerticalCanvasDockStopVirtualCam",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.StopVirtualCam"))).toUtf8().constData(),
start_virtual_cam_hotkey, stop_virtual_cam_hotkey, this, this);
start_hotkey = obs_data_get_array(settings, "start_virtual_cam_hotkey");
stop_hotkey = obs_data_get_array(settings, "stop_virtual_cam_hotkey");
obs_hotkey_pair_load(virtual_cam_hotkey, start_hotkey, stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
record_hotkey = obs_hotkey_pair_register_frontend(
"VerticalCanvasDockStartRecording",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.StartRecording"))).toUtf8().constData(),
"VerticalCanvasDockStopRecording",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.StopRecording"))).toUtf8().constData(),
start_recording_hotkey, stop_recording_hotkey, this, this);
start_hotkey = obs_data_get_array(settings, "start_record_hotkey");
stop_hotkey = obs_data_get_array(settings, "stop_record_hotkey");
obs_hotkey_pair_load(record_hotkey, start_hotkey, stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
stream_hotkey = obs_hotkey_pair_register_frontend(
"VerticalCanvasDockStartStreaming",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.StartStreaming"))).toUtf8().constData(),
"VerticalCanvasDockStopStreaming",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.StopStreaming"))).toUtf8().constData(),
start_streaming_hotkey, stop_streaming_hotkey, this, this);
start_hotkey = obs_data_get_array(settings, "start_stream_hotkey");
stop_hotkey = obs_data_get_array(settings, "stop_stream_hotkey");
obs_hotkey_pair_load(stream_hotkey, start_hotkey, stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
pause_hotkey = obs_hotkey_pair_register_frontend(
"VerticalCanvasDockPause",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.PauseRecording"))).toUtf8().constData(),
"VerticalCanvasDockUnpause",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.UnpauseRecording")))
.toUtf8()
.constData(),
pause_recording_hotkey, unpause_recording_hotkey, this, this);
start_hotkey = obs_data_get_array(settings, "pause_hotkey");
stop_hotkey = obs_data_get_array(settings, "unpause_hotkey");
obs_hotkey_pair_load(pause_hotkey, start_hotkey, stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
preview_hotkey = obs_hotkey_pair_register_frontend(
"VerticalCanvasDockShowPreview",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.PreviewConextMenu.Enable")))
.toUtf8()
.constData(),
"VerticalCanvasDockHidePreview",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.Preview.Disable"))).toUtf8().constData(),
show_preview_hotkey, hide_preview_hotkey, this, this);
start_hotkey = obs_data_get_array(settings, "show_preview_hotkey");
stop_hotkey = obs_data_get_array(settings, "hide_preview_hotkey");
obs_hotkey_pair_load(preview_hotkey, start_hotkey, stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
chapter_hotkey = obs_hotkey_register_frontend(
"VerticalCanvasDockChapter",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.AddChapterMarker")))
.toUtf8()
.constData(),
recording_chapter_hotkey, this);
start_hotkey = obs_data_get_array(settings, "chapter_hotkey");
obs_hotkey_load(chapter_hotkey, start_hotkey);
obs_data_array_release(start_hotkey);
split_hotkey = obs_hotkey_register_frontend(
"VerticalCanvasDockSplit",
(title + " " + QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.SplitFile"))).toUtf8().constData(),
recording_split_hotkey, this);
start_hotkey = obs_data_get_array(settings, "split_hotkey");
obs_hotkey_load(split_hotkey, start_hotkey);
obs_data_array_release(start_hotkey);
if (first_time) {
obs_data_release(settings);
}
hide();
transitionAudioWrapper =
obs_source_create_private("vertical_audio_wrapper_source", "vertical_audio_wrapper_source", nullptr);
obs_source_set_muted(transitionAudioWrapper, true);
auto aw = (struct audio_wrapper_info *)obs_obj_get_data(transitionAudioWrapper);
if (aw) {
aw->param = this;
aw->target = [](void *param) {
CanvasDock *dock = reinterpret_cast<CanvasDock *>(param);
if (std::find(canvas_docks.begin(), canvas_docks.end(), dock) == canvas_docks.end()) {
return (obs_source_t *)nullptr;
}
return obs_weak_source_get_source(dock->source);
};
}
obs_frontend_add_save_callback(save_load, this);
}
CanvasDock::~CanvasDock()
{
obs_frontend_remove_save_callback(save_load, this);
for (auto projector : projectors) {
delete projector;
}
canvas_docks.remove(this);
obs_hotkey_pair_unregister(backtrack_hotkey);
obs_hotkey_pair_unregister(virtual_cam_hotkey);
obs_hotkey_pair_unregister(record_hotkey);
obs_hotkey_pair_unregister(stream_hotkey);
obs_hotkey_pair_unregister(pause_hotkey);
obs_hotkey_pair_unregister(preview_hotkey);
obs_hotkey_unregister(chapter_hotkey);
obs_hotkey_unregister(split_hotkey);
obs_display_remove_draw_callback(preview->GetDisplay(), DrawPreview, this);
for (uint32_t i = MAX_CHANNELS - 1; i > 0; i--) {
auto s = obs_get_output_source(i);
if (s == transitionAudioWrapper) {
obs_source_release(s);
obs_set_output_source(i, nullptr);
break;
}
obs_source_release(s);
}
obs_source_release(transitionAudioWrapper);
transitionAudioWrapper = nullptr;
sourcesDock = nullptr;
scenesDock = nullptr;
transitionsDock = nullptr;
auto sh = obs_get_signal_handler();
signal_handler_disconnect(sh, "source_rename", source_rename, this);
signal_handler_disconnect(sh, "source_remove", source_remove, this);
signal_handler_disconnect(sh, "source_destroy", source_remove, this);
//signal_handler_disconnect(sh, "source_create", source_create, this);
//signal_handler_disconnect(sh, "source_load", source_load, this);
signal_handler_disconnect(sh, "source_save", source_save, this);
if (obs_output_active(recordOutput)) {
obs_output_stop(recordOutput);
}
obs_output_release(recordOutput);
recordOutput = nullptr;
if (replayOutput) {
auto rpsh = obs_output_get_signal_handler(replayOutput);
signal_handler_disconnect(rpsh, "saved", replay_saved, this);
}
if (obs_output_active(replayOutput)) {
obs_output_stop(replayOutput);
}
obs_output_release(replayOutput);
replayOutput = nullptr;
if (obs_output_active(virtualCamOutput)) {
obs_output_stop(virtualCamOutput);
}
obs_output_release(virtualCamOutput);
virtualCamOutput = nullptr;
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (obs_output_active(it->output)) {
obs_output_stop(it->output);
}
obs_service_release(obs_output_get_service(it->output));
obs_output_release(it->output);
obs_data_release(it->settings);
}
streamOutputs.clear();
obs_data_release(stream_encoder_settings);
obs_data_release(record_encoder_settings);
if (multiCanvasSource) {
multi_canvas_source_remove_canvas(obs_obj_get_data(multiCanvasSource), canvas);
obs_source_release(multiCanvasSource);
multiCanvasSource = nullptr;
}
if (multiCanvasVideo) {
multiCanvasVideo = nullptr;
obs_canvas_set_channel(multiCanvas, 0, nullptr);
}
if (multiCanvas) {
obs_canvas_remove(multiCanvas);
obs_canvas_release(multiCanvas);
multiCanvas = nullptr;
}
auto ph = obs_get_proc_handler();
calldata_t cd = {0};
calldata_set_string(&cd, "canvas_name", CANVAS_NAME);
proc_handler_call(ph, "downstream_keyer_remove_canvas", &cd);
calldata_free(&cd);
DestroyVideo();
if (canvas) {
obs_canvas_remove(canvas);
obs_canvas_release(canvas);
canvas = nullptr;
}
obs_enter_graphics();
if (overflow) {
gs_texture_destroy(overflow);
}
if (rectFill) {
gs_vertexbuffer_destroy(rectFill);
}
if (circleFill) {
gs_vertexbuffer_destroy(circleFill);
}
gs_vertexbuffer_destroy(box);
obs_leave_graphics();
obs_source_t *oldTransition = obs_weak_source_get_source(source);
if (oldTransition && obs_source_get_type(oldTransition) == OBS_SOURCE_TYPE_TRANSITION) {
obs_weak_source_release(source);
source = nullptr;
signal_handler_t *handler = obs_source_get_signal_handler(oldTransition);
signal_handler_disconnect(handler, "transition_stop", transition_override_stop, this);
obs_source_dec_showing(oldTransition);
obs_source_dec_active(oldTransition);
}
obs_source_release(oldTransition);
transitions.clear();
}
void CanvasDock::setAction(QAction *a)
{
action = a;
}
static bool SceneItemHasVideo(obs_sceneitem_t *item)
{
const obs_source_t *source = obs_sceneitem_get_source(item);
const uint32_t flags = obs_source_get_output_flags(source);
return (flags & OBS_SOURCE_VIDEO) != 0;
}
config_t *get_user_config(void)
{
return obs_frontend_get_user_config();
}
void CanvasDock::DrawOverflow(float scale)
{
if (locked) {
return;
}
auto config = get_user_config();
if (!config) {
return;
}
bool hidden = config_get_bool(config, "BasicWindow", "OverflowHidden");
if (hidden) {
return;
}
GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawOverflow");
if (!overflow) {
const auto file = obs_module_file("images/overflow.png");
overflow = gs_texture_create_from_file(file);
bfree(file);
}
if (scene) {
gs_matrix_push();
gs_matrix_scale3f(scale, scale, 1.0f);
obs_scene_enum_items(scene, DrawSelectedOverflow, this);
gs_matrix_pop();
}
gs_load_vertexbuffer(nullptr);
GS_DEBUG_MARKER_END();
}
static bool CloseFloat(float a, float b, float epsilon = 0.01f)
{
return std::abs(a - b) <= epsilon;
}
bool CanvasDock::DrawSelectedOverflow(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
if (obs_sceneitem_locked(item)) {
return true;
}
if (!SceneItemHasVideo(item)) {
return true;
}
auto config = get_user_config();
if (!config) {
return true;
}
bool select = config_get_bool(config, "BasicWindow", "OverflowSelectionHidden");
if (!select && !obs_sceneitem_visible(item)) {
return true;
}
if (obs_sceneitem_is_group(item)) {
matrix4 mat;
obs_sceneitem_get_draw_transform(item, &mat);
gs_matrix_push();
gs_matrix_mul(&mat);
obs_sceneitem_group_enum_items(item, DrawSelectedOverflow, param);
gs_matrix_pop();
}
bool always = config_get_bool(config, "BasicWindow", "OverflowAlwaysVisible");
if (!always && !obs_sceneitem_selected(item)) {
return true;
}
CanvasDock *prev = reinterpret_cast<CanvasDock *>(param);
matrix4 boxTransform;
matrix4 invBoxTransform;
obs_sceneitem_get_box_transform(item, &boxTransform);
matrix4_inv(&invBoxTransform, &boxTransform);
vec3 bounds[] = {
{{{0.f, 0.f, 0.f}}},
{{{1.f, 0.f, 0.f}}},
{{{0.f, 1.f, 0.f}}},
{{{1.f, 1.f, 0.f}}},
};
bool visible = std::all_of(std::begin(bounds), std::end(bounds), [&](const vec3 &b) {
vec3 pos;
vec3_transform(&pos, &b, &boxTransform);
vec3_transform(&pos, &pos, &invBoxTransform);
return CloseFloat(pos.x, b.x) && CloseFloat(pos.y, b.y);
});
if (!visible) {
return true;
}
GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawSelectedOverflow");
gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_REPEAT);
gs_eparam_t *image = gs_effect_get_param_by_name(solid, "image");
gs_eparam_t *scale = gs_effect_get_param_by_name(solid, "scale");
vec2 s;
vec2_set(&s, boxTransform.x.x / 96, boxTransform.y.y / 96);
gs_effect_set_vec2(scale, &s);
gs_effect_set_texture(image, prev->overflow);
gs_matrix_push();
gs_matrix_mul(&boxTransform);
obs_sceneitem_crop crop;
obs_sceneitem_get_crop(item, &crop);
while (gs_effect_loop(solid, "Draw")) {
gs_draw_sprite(prev->overflow, 0, 1, 1);
}
gs_matrix_pop();
GS_DEBUG_MARKER_END();
UNUSED_PARAMETER(scene);
return true;
}
void CanvasDock::DrawBackdrop(float cx, float cy)
{
if (!box) {
return;
}
GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop");
gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color");
gs_technique_t *tech = gs_effect_get_technique(solid, "Solid");
vec4 colorVal;
vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f);
gs_effect_set_vec4(color, &colorVal);
gs_technique_begin(tech);
gs_technique_begin_pass(tech, 0);
gs_matrix_push();
gs_matrix_identity();
gs_matrix_scale3f(float(cx), float(cy), 1.0f);
gs_load_vertexbuffer(box);
gs_draw(GS_TRISTRIP, 0, 0);
gs_matrix_pop();
gs_technique_end_pass(tech);
gs_technique_end(tech);
gs_load_vertexbuffer(nullptr);
GS_DEBUG_MARKER_END();
}
void CanvasDock::DrawPreview(void *data, uint32_t cx, uint32_t cy)
{
CanvasDock *window = static_cast<CanvasDock *>(data);
uint32_t sourceCX = window->canvas_width;
if (sourceCX <= 0) {
sourceCX = 1;
}
uint32_t sourceCY = window->canvas_height;
if (sourceCY <= 0) {
sourceCY = 1;
}
int x, y;
float scale;
GetScaleAndCenterPos(sourceCX, sourceCY, cx, cy, x, y, scale);
if (window->previewScale != scale) {
window->previewScale = scale;
}
auto newCX = scale * float(sourceCX);
auto newCY = scale * float(sourceCY);
/* auto extraCx = (window->zoom - 1.0f) * newCX;
auto extraCy = (window->zoom - 1.0f) * newCY;
int newCx = newCX * window->zoom;
int newCy = newCY * window->zoom;
x -= extraCx * window->scrollX;
y -= extraCy * window->scrollY;*/
gs_viewport_push();
gs_projection_push();
gs_ortho(float(-x), newCX + float(x), float(-y), newCY + float(y), -100.0f, 100.0f);
gs_reset_viewport();
window->DrawOverflow(scale);
window->DrawBackdrop(newCX, newCY);
const bool previous = gs_set_linear_srgb(true);
gs_ortho(0.0f, float(sourceCX), 0.0f, float(sourceCY), -100.0f, 100.0f);
gs_set_viewport(x, y, (int)newCX, (int)newCY);
if (window->canvas) {
obs_canvas_render(window->canvas);
}
gs_set_linear_srgb(previous);
gs_ortho(float(-x), newCX + float(x), float(-y), newCY + float(y), -100.0f, 100.0f);
gs_reset_viewport();
gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
gs_technique_t *tech = gs_effect_get_technique(solid, "Solid");
gs_technique_begin(tech);
gs_technique_begin_pass(tech, 0);
if (window->scene && !window->locked) {
gs_matrix_push();
gs_matrix_scale3f(scale, scale, 1.0f);
obs_scene_enum_items(window->scene, DrawSelectedItem, data);
gs_matrix_pop();
}
if (window->selectionBox) {
if (!window->rectFill) {
gs_render_start(true);
gs_vertex2f(0.0f, 0.0f);
gs_vertex2f(1.0f, 0.0f);
gs_vertex2f(0.0f, 1.0f);
gs_vertex2f(1.0f, 1.0f);
window->rectFill = gs_render_save();
}
window->DrawSelectionBox(window->startPos.x * scale, window->startPos.y * scale, window->mousePos.x * scale,
window->mousePos.y * scale, window->rectFill);
}
gs_load_vertexbuffer(nullptr);
gs_technique_end_pass(tech);
gs_technique_end(tech);
if (window->drawSpacingHelpers) {
window->DrawSpacingHelpers(window->scene, (float)x, (float)y, newCX, newCY, scale, float(sourceCX),
float(sourceCY));
}
gs_projection_pop();
gs_viewport_pop();
}
struct SceneFindData {
const vec2 &pos;
OBSSceneItem item;
bool selectBelow;
obs_sceneitem_t *group = nullptr;
SceneFindData(const SceneFindData &) = delete;
SceneFindData(SceneFindData &&) = delete;
SceneFindData &operator=(const SceneFindData &) = delete;
SceneFindData &operator=(SceneFindData &&) = delete;
inline SceneFindData(const vec2 &pos_, bool selectBelow_) : pos(pos_), selectBelow(selectBelow_) {}
};
struct SceneFindBoxData {
const vec2 &startPos;
const vec2 &pos;
std::vector<obs_sceneitem_t *> sceneItems;
SceneFindBoxData(const SceneFindData &) = delete;
SceneFindBoxData(SceneFindData &&) = delete;
SceneFindBoxData &operator=(const SceneFindData &) = delete;
SceneFindBoxData &operator=(SceneFindData &&) = delete;
inline SceneFindBoxData(const vec2 &startPos_, const vec2 &pos_) : startPos(startPos_), pos(pos_) {}
};
bool CanvasDock::FindSelected(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
SceneFindBoxData *data = reinterpret_cast<SceneFindBoxData *>(param);
if (obs_sceneitem_selected(item)) {
data->sceneItems.push_back(item);
}
UNUSED_PARAMETER(scene);
return true;
}
static vec2 GetItemSize(obs_sceneitem_t *item)
{
obs_bounds_type boundsType = obs_sceneitem_get_bounds_type(item);
vec2 size;
if (boundsType != OBS_BOUNDS_NONE) {
obs_sceneitem_get_bounds(item, &size);
} else {
obs_source_t *source = obs_sceneitem_get_source(item);
obs_sceneitem_crop crop;
vec2 scale;
obs_sceneitem_get_scale(item, &scale);
obs_sceneitem_get_crop(item, &crop);
size.x = float(obs_source_get_width(source) - crop.left - crop.right) * scale.x;
size.y = float(obs_source_get_height(source) - crop.top - crop.bottom) * scale.y;
}
return size;
}
static vec3 GetTransformedPos(float x, float y, const matrix4 &mat)
{
vec3 result;
vec3_set(&result, x, y, 0.0f);
vec3_transform(&result, &result, &mat);
return result;
}
static void DrawLine(float x1, float y1, float x2, float y2, float thickness, vec2 scale)
{
float ySide = (y1 == y2) ? (y1 < 0.5f ? 1.0f : -1.0f) : 0.0f;
float xSide = (x1 == x2) ? (x1 < 0.5f ? 1.0f : -1.0f) : 0.0f;
gs_render_start(true);
gs_vertex2f(x1, y1);
gs_vertex2f(x1 + (xSide * (thickness / scale.x)), y1 + (ySide * (thickness / scale.y)));
gs_vertex2f(x2 + (xSide * (thickness / scale.x)), y2 + (ySide * (thickness / scale.y)));
gs_vertex2f(x2, y2);
gs_vertex2f(x1, y1);
gs_vertbuffer_t *line = gs_render_save();
gs_load_vertexbuffer(line);
gs_draw(GS_TRISTRIP, 0, 0);
gs_vertexbuffer_destroy(line);
}
void CanvasDock::DrawSpacingLine(vec3 &start, vec3 &end, vec3 &viewport, float pixelRatio)
{
matrix4 transform;
matrix4_identity(&transform);
transform.x.x = viewport.x;
transform.y.y = viewport.y;
gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
gs_technique_t *tech = gs_effect_get_technique(solid, "Solid");
QColor selColor = GetSelectionColor();
vec4 color;
vec4_set(&color, selColor.redF(), selColor.greenF(), selColor.blueF(), 1.0f);
gs_effect_set_vec4(gs_effect_get_param_by_name(solid, "color"), &color);
gs_technique_begin(tech);
gs_technique_begin_pass(tech, 0);
gs_matrix_push();
gs_matrix_mul(&transform);
vec2 scale;
vec2_set(&scale, viewport.x, viewport.y);
DrawLine(start.x, start.y, end.x, end.y, pixelRatio * (HANDLE_RADIUS / 2), scale);
gs_matrix_pop();
gs_load_vertexbuffer(nullptr);
gs_technique_end_pass(tech);
gs_technique_end(tech);
}
void CanvasDock::SetLabelText(int sourceIndex, int px)
{
if (px == spacerPx[sourceIndex]) {
return;
}
std::string text = std::to_string(px) + " px";
obs_source_t *s = spacerLabel[sourceIndex];
OBSDataAutoRelease settings = obs_source_get_settings(s);
obs_data_set_string(settings, "text", text.c_str());
obs_source_update(s, settings);
spacerPx[sourceIndex] = px;
}
static void DrawLabel(OBSSource source, vec3 &pos, vec3 &viewport)
{
if (!source) {
return;
}
vec3_mul(&pos, &pos, &viewport);
gs_matrix_push();
gs_matrix_identity();
gs_matrix_translate(&pos);
obs_source_video_render(source);
gs_matrix_pop();
}
void CanvasDock::RenderSpacingHelper(int sourceIndex, vec3 &start, vec3 &end, vec3 &viewport, float pixelRatio)
{
bool horizontal = (sourceIndex == 2 || sourceIndex == 3);
// If outside of preview, don't render
if (!((horizontal && (end.x >= start.x)) || (!horizontal && (end.y >= start.y)))) {
return;
}
float length = vec3_dist(&start, &end);
float px;
if (horizontal) {
px = length * (float)canvas_width;
} else {
px = length * (float)canvas_height;
}
if (px <= 0.0f) {
return;
}
obs_source_t *s = spacerLabel[sourceIndex];
vec3 labelSize, labelPos;
vec3_set(&labelSize, (float)obs_source_get_width(s), (float)obs_source_get_height(s), 1.0f);
vec3_div(&labelSize, &labelSize, &viewport);
vec3 labelMargin;
vec3_set(&labelMargin, SPACER_LABEL_MARGIN * pixelRatio, SPACER_LABEL_MARGIN * pixelRatio, 1.0f);
vec3_div(&labelMargin, &labelMargin, &viewport);
vec3_set(&labelPos, end.x, end.y, end.z);
if (horizontal) {
labelPos.x -= (end.x - start.x) / 2;
labelPos.x -= labelSize.x / 2;
labelPos.y -= labelMargin.y + (labelSize.y / 2) + (HANDLE_RADIUS / viewport.y);
} else {
labelPos.y -= (end.y - start.y) / 2;
labelPos.y -= labelSize.y / 2;
labelPos.x += labelMargin.x;
}
DrawSpacingLine(start, end, viewport, pixelRatio);
SetLabelText(sourceIndex, (int)px);
DrawLabel(s, labelPos, viewport);
}
static obs_source_t *CreateLabel(float pixelRatio, int i)
{
OBSDataAutoRelease settings = obs_data_create();
OBSDataAutoRelease font = obs_data_create();
#if defined(_WIN32)
obs_data_set_string(font, "face", "Arial");
#elif defined(__APPLE__)
obs_data_set_string(font, "face", "Helvetica");
#else
obs_data_set_string(font, "face", "Monospace");
#endif
obs_data_set_int(font, "flags", 1); // Bold text
obs_data_set_int(font, "size", (int)(16.0f * pixelRatio));
obs_data_set_obj(settings, "font", font);
obs_data_set_bool(settings, "outline", true);
#ifdef _WIN32
obs_data_set_int(settings, "outline_color", 0x000000);
obs_data_set_int(settings, "outline_size", 3);
const char *text_source_id = "text_gdiplus";
#else
const char *text_source_id = "text_ft2_source";
#endif
struct dstr name;
dstr_init(&name);
dstr_printf(&name, "Vulcast Vertical Preview spacing label %d", i);
OBSSource txtSource = obs_source_create_private(text_source_id, name.array, settings);
dstr_free(&name);
return txtSource;
}
obs_scene_item *CanvasDock::GetSelectedItem(obs_scene_t *s)
{
vec2 pos;
SceneFindBoxData sfbd(pos, pos);
if (!s) {
s = this->scene;
}
obs_scene_enum_items(s, FindSelected, &sfbd);
if (sfbd.sceneItems.size() != 1) {
return nullptr;
}
return sfbd.sceneItems.at(0);
}
void CanvasDock::DrawSpacingHelpers(obs_scene_t *s, float x, float y, float cx, float cy, float scale, float sourceX, float sourceY)
{
UNUSED_PARAMETER(x);
UNUSED_PARAMETER(y);
if (locked) {
return;
}
OBSSceneItem item = GetSelectedItem();
if (!item) {
return;
}
if (obs_sceneitem_locked(item)) {
return;
}
vec2 itemSize = GetItemSize(item);
if (itemSize.x == 0.0f || itemSize.y == 0.0f) {
return;
}
obs_sceneitem_t *parentGroup = obs_sceneitem_get_group(s, item);
if (parentGroup && obs_sceneitem_locked(parentGroup)) {
return;
}
matrix4 boxTransform;
obs_sceneitem_get_box_transform(item, &boxTransform);
vec3 size;
vec3_set(&size, sourceX, sourceY, 1.0f);
// Init box transform side locations
vec3 left, right, top, bottom;
vec3_set(&left, 0.0f, 0.5f, 1.0f);
vec3_set(&right, 1.0f, 0.5f, 1.0f);
vec3_set(&top, 0.5f, 0.0f, 1.0f);
vec3_set(&bottom, 0.5f, 1.0f, 1.0f);
// Decide which side to use with box transform, based on rotation
// Seems hacky, probably a better way to do it
float rot = obs_sceneitem_get_rot(item);
if (parentGroup) {
//Correct the scene item rotation angle
rot += obs_sceneitem_get_rot(parentGroup);
vec2 group_scale;
obs_sceneitem_get_scale(parentGroup, &group_scale);
vec2 group_pos;
obs_sceneitem_get_pos(parentGroup, &group_pos);
// Correct the scene item box transform
// Based on scale, rotation angle, position of parent's group
matrix4_scale3f(&boxTransform, &boxTransform, group_scale.x, group_scale.y, 1.0f);
matrix4_rotate_aa4f(&boxTransform, &boxTransform, 0.0f, 0.0f, 1.0f, RAD(obs_sceneitem_get_rot(parentGroup)));
matrix4_translate3f(&boxTransform, &boxTransform, group_pos.x, group_pos.y, 0.0f);
}
if (rot >= HELPER_ROT_BREAKPONT) {
for (float i = HELPER_ROT_BREAKPONT; i <= 360.0f; i += 90.0f) {
if (rot < i) {
break;
}
vec3 l = left;
vec3 r = right;
vec3 t = top;
vec3 b = bottom;
vec3_copy(&top, &l);
vec3_copy(&right, &t);
vec3_copy(&bottom, &r);
vec3_copy(&left, &b);
}
} else if (rot <= -HELPER_ROT_BREAKPONT) {
for (float i = -HELPER_ROT_BREAKPONT; i >= -360.0f; i -= 90.0f) {
if (rot > i) {
break;
}
vec3 l = left;
vec3 r = right;
vec3 t = top;
vec3 b = bottom;
vec3_copy(&top, &r);
vec3_copy(&right, &b);
vec3_copy(&bottom, &l);
vec3_copy(&left, &t);
}
}
vec2 item_scale;
obs_sceneitem_get_scale(item, &item_scale);
// Switch top/bottom or right/left if scale is negative
if (item_scale.x < 0.0f) {
vec3 l = left;
vec3 r = right;
vec3_copy(&left, &r);
vec3_copy(&right, &l);
}
if (item_scale.y < 0.0f) {
vec3 t = top;
vec3 b = bottom;
vec3_copy(&top, &b);
vec3_copy(&bottom, &t);
}
// Get sides of box transform
left = GetTransformedPos(left.x, left.y, boxTransform);
right = GetTransformedPos(right.x, right.y, boxTransform);
top = GetTransformedPos(top.x, top.y, boxTransform);
bottom = GetTransformedPos(bottom.x, bottom.y, boxTransform);
bottom.y = size.y - bottom.y;
right.x = size.x - right.x;
// Init viewport
vec3 viewport;
vec3_set(&viewport, cx, cy, 1.0f);
vec3_div(&left, &left, &viewport);
vec3_div(&right, &right, &viewport);
vec3_div(&top, &top, &viewport);
vec3_div(&bottom, &bottom, &viewport);
vec3_mulf(&left, &left, scale);
vec3_mulf(&right, &right, scale);
vec3_mulf(&top, &top, scale);
vec3_mulf(&bottom, &bottom, scale);
// Draw spacer lines and labels
vec3 start, end;
float pixelRatio = 1.0f; //main->GetDevicePixelRatio();
if (!spacerLabel[3]) {
QMetaObject::invokeMethod(this, [this, pixelRatio]() {
for (int i = 0; i < 4; i++) {
if (!spacerLabel[i]) {
spacerLabel[i] = CreateLabel(pixelRatio, i);
}
}
});
return;
}
vec3_set(&start, top.x, 0.0f, 1.0f);
vec3_set(&end, top.x, top.y, 1.0f);
RenderSpacingHelper(0, start, end, viewport, pixelRatio);
vec3_set(&start, bottom.x, 1.0f - bottom.y, 1.0f);
vec3_set(&end, bottom.x, 1.0f, 1.0f);
RenderSpacingHelper(1, start, end, viewport, pixelRatio);
vec3_set(&start, 0.0f, left.y, 1.0f);
vec3_set(&end, left.x, left.y, 1.0f);
RenderSpacingHelper(2, start, end, viewport, pixelRatio);
vec3_set(&start, 1.0f - right.x, right.y, 1.0f);
vec3_set(&end, 1.0f, right.y, 1.0f);
RenderSpacingHelper(3, start, end, viewport, pixelRatio);
}
static inline bool crop_enabled(const obs_sceneitem_crop *crop)
{
return crop->left > 0 || crop->top > 0 || crop->right > 0 || crop->bottom > 0;
}
static void DrawSquareAtPos(float x, float y, float pixelRatio)
{
struct vec3 pos;
vec3_set(&pos, x, y, 0.0f);
struct matrix4 matrix;
gs_matrix_get(&matrix);
vec3_transform(&pos, &pos, &matrix);
gs_matrix_push();
gs_matrix_identity();
gs_matrix_translate(&pos);
gs_matrix_translate3f(-HANDLE_RADIUS * pixelRatio, -HANDLE_RADIUS * pixelRatio, 0.0f);
gs_matrix_scale3f(HANDLE_RADIUS * pixelRatio * 2, HANDLE_RADIUS * pixelRatio * 2, 1.0f);
gs_draw(GS_TRISTRIP, 0, 0);
gs_matrix_pop();
}
static void DrawRotationHandle(gs_vertbuffer_t *circle, float rot, float pixelRatio)
{
struct vec3 pos;
vec3_set(&pos, 0.5f, 0.0f, 0.0f);
struct matrix4 matrix;
gs_matrix_get(&matrix);
vec3_transform(&pos, &pos, &matrix);
gs_render_start(true);
gs_vertex2f(0.5f - 0.34f / HANDLE_RADIUS, 0.5f);
gs_vertex2f(0.5f - 0.34f / HANDLE_RADIUS, -2.0f);
gs_vertex2f(0.5f + 0.34f / HANDLE_RADIUS, -2.0f);
gs_vertex2f(0.5f + 0.34f / HANDLE_RADIUS, 0.5f);
gs_vertex2f(0.5f - 0.34f / HANDLE_RADIUS, 0.5f);
gs_vertbuffer_t *line = gs_render_save();
gs_load_vertexbuffer(line);
gs_matrix_push();
gs_matrix_identity();
gs_matrix_translate(&pos);
gs_matrix_rotaa4f(0.0f, 0.0f, 1.0f, RAD(rot));
gs_matrix_translate3f(-HANDLE_RADIUS * 1.5f * pixelRatio, -HANDLE_RADIUS * 1.5f * pixelRatio, 0.0f);
gs_matrix_scale3f(HANDLE_RADIUS * 3 * pixelRatio, HANDLE_RADIUS * 3 * pixelRatio, 1.0f);
gs_draw(GS_TRISTRIP, 0, 0);
gs_matrix_translate3f(0.0f, -HANDLE_RADIUS * 2 / 3, 0.0f);
gs_load_vertexbuffer(circle);
gs_draw(GS_TRISTRIP, 0, 0);
gs_matrix_pop();
gs_vertexbuffer_destroy(line);
}
static void DrawStripedLine(float x1, float y1, float x2, float y2, float thickness, vec2 scale)
{
float ySide = (y1 == y2) ? (y1 < 0.5f ? 1.0f : -1.0f) : 0.0f;
float xSide = (x1 == x2) ? (x1 < 0.5f ? 1.0f : -1.0f) : 0.0f;
float dist = sqrtf(powf((x1 - x2) * scale.x, 2.0f) + powf((y1 - y2) * scale.y, 2.0f));
if (dist > 1000000.0f) {
// too many stripes to draw, draw it as a line as fallback
DrawLine(x1, y1, x2, y2, thickness, scale);
return;
}
float offX = (x2 - x1) / dist;
float offY = (y2 - y1) / dist;
for (int i = 0, l = (int)ceil(dist / 15.0); i < l; i++) {
gs_render_start(true);
float xx1 = x1 + (float)i * 15.0f * offX;
float yy1 = y1 + (float)i * 15.0f * offY;
float dx;
float dy;
if (x1 < x2) {
dx = std::min(xx1 + 7.5f * offX, x2);
} else {
dx = std::max(xx1 + 7.5f * offX, x2);
}
if (y1 < y2) {
dy = std::min(yy1 + 7.5f * offY, y2);
} else {
dy = std::max(yy1 + 7.5f * offY, y2);
}
gs_vertex2f(xx1, yy1);
gs_vertex2f(xx1 + (xSide * (thickness / scale.x)), yy1 + (ySide * (thickness / scale.y)));
gs_vertex2f(dx, dy);
gs_vertex2f(dx + (xSide * (thickness / scale.x)), dy + (ySide * (thickness / scale.y)));
gs_vertbuffer_t *line = gs_render_save();
gs_load_vertexbuffer(line);
gs_draw(GS_TRISTRIP, 0, 0);
gs_vertexbuffer_destroy(line);
}
}
static void DrawRect(float thickness, vec2 scale)
{
if (scale.x <= 0.0f || scale.y <= 0.0f || thickness <= 0.0f) {
return;
}
gs_render_start(true);
gs_vertex2f(0.0f, 0.0f);
gs_vertex2f(0.0f + (thickness / scale.x), 0.0f);
gs_vertex2f(0.0f, 1.0f);
gs_vertex2f(0.0f + (thickness / scale.x), 1.0f);
gs_vertex2f(0.0f, 1.0f - (thickness / scale.y));
gs_vertex2f(1.0f, 1.0f);
gs_vertex2f(1.0f, 1.0f - (thickness / scale.y));
gs_vertex2f(1.0f - (thickness / scale.x), 1.0f);
gs_vertex2f(1.0f, 0.0f);
gs_vertex2f(1.0f - (thickness / scale.x), 0.0f);
gs_vertex2f(1.0f, 0.0f + (thickness / scale.y));
gs_vertex2f(0.0f, 0.0f);
gs_vertex2f(0.0f, 0.0f + (thickness / scale.y));
gs_vertbuffer_t *rect = gs_render_save();
gs_load_vertexbuffer(rect);
gs_draw(GS_TRISTRIP, 0, 0);
gs_vertexbuffer_destroy(rect);
}
bool CanvasDock::DrawSelectedItem(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
if (obs_sceneitem_locked(item)) {
return true;
}
if (!SceneItemHasVideo(item)) {
return true;
}
CanvasDock *window = static_cast<CanvasDock *>(param);
if (obs_sceneitem_is_group(item)) {
matrix4 mat;
obs_sceneitem_get_draw_transform(item, &mat);
window->groupRot = obs_sceneitem_get_rot(item);
gs_matrix_push();
gs_matrix_mul(&mat);
obs_sceneitem_group_enum_items(item, DrawSelectedItem, param);
gs_matrix_pop();
window->groupRot = 0.0f;
}
float pixelRatio = window->GetDevicePixelRatio();
bool hovered = false;
{
std::lock_guard<std::mutex> lock(window->selectMutex);
for (size_t i = 0; i < window->hoveredPreviewItems.size(); i++) {
if (window->hoveredPreviewItems[i] == item) {
hovered = true;
break;
}
}
}
bool selected = obs_sceneitem_selected(item);
if (!selected && !hovered) {
return true;
}
matrix4 boxTransform;
matrix4 invBoxTransform;
obs_sceneitem_get_box_transform(item, &boxTransform);
matrix4_inv(&invBoxTransform, &boxTransform);
vec3 bounds[] = {
{{{0.f, 0.f, 0.f}}},
{{{1.f, 0.f, 0.f}}},
{{{0.f, 1.f, 0.f}}},
{{{1.f, 1.f, 0.f}}},
};
//main->GetCameraIcon();
QColor selColor = window->GetSelectionColor();
QColor cropColor = window->GetCropColor();
QColor hoverColor = window->GetHoverColor();
vec4 red;
vec4 green;
vec4 blue;
vec4_set(&red, selColor.redF(), selColor.greenF(), selColor.blueF(), 1.0f);
vec4_set(&green, cropColor.redF(), cropColor.greenF(), cropColor.blueF(), 1.0f);
vec4_set(&blue, hoverColor.redF(), hoverColor.greenF(), hoverColor.blueF(), 1.0f);
bool visible = std::all_of(std::begin(bounds), std::end(bounds), [&](const vec3 &b) {
vec3 pos;
vec3_transform(&pos, &b, &boxTransform);
vec3_transform(&pos, &pos, &invBoxTransform);
return CloseFloat(pos.x, b.x) && CloseFloat(pos.y, b.y);
});
if (!visible) {
return true;
}
GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawSelectedItem");
matrix4 curTransform;
vec2 boxScale;
gs_matrix_get(&curTransform);
obs_sceneitem_get_box_scale(item, &boxScale);
boxScale.x *= curTransform.x.x;
boxScale.y *= curTransform.y.y;
gs_matrix_push();
gs_matrix_mul(&boxTransform);
obs_sceneitem_crop crop;
obs_sceneitem_get_crop(item, &crop);
gs_effect_t *eff = gs_get_effect();
gs_eparam_t *colParam = gs_effect_get_param_by_name(eff, "color");
gs_effect_set_vec4(colParam, &red);
if (obs_sceneitem_get_bounds_type(item) == OBS_BOUNDS_NONE && crop_enabled(&crop)) {
#define DRAW_SIDE(side, x1, y1, x2, y2) \
if (hovered && !selected) { \
gs_effect_set_vec4(colParam, &blue); \
DrawLine(x1, y1, x2, y2, HANDLE_RADIUS *pixelRatio / 2, boxScale); \
} else if (crop.side > 0) { \
gs_effect_set_vec4(colParam, &green); \
DrawStripedLine(x1, y1, x2, y2, HANDLE_RADIUS *pixelRatio / 2, boxScale); \
} else { \
DrawLine(x1, y1, x2, y2, HANDLE_RADIUS *pixelRatio / 2, boxScale); \
} \
gs_effect_set_vec4(colParam, &red);
DRAW_SIDE(left, 0.0f, 0.0f, 0.0f, 1.0f);
DRAW_SIDE(top, 0.0f, 0.0f, 1.0f, 0.0f);
DRAW_SIDE(right, 1.0f, 0.0f, 1.0f, 1.0f);
DRAW_SIDE(bottom, 0.0f, 1.0f, 1.0f, 1.0f);
#undef DRAW_SIDE
} else {
if (!selected) {
gs_effect_set_vec4(colParam, &blue);
DrawRect(HANDLE_RADIUS * pixelRatio / 2, boxScale);
} else {
DrawRect(HANDLE_RADIUS * pixelRatio / 2, boxScale);
}
}
gs_load_vertexbuffer(window->box);
gs_effect_set_vec4(colParam, &red);
if (selected) {
DrawSquareAtPos(0.0f, 0.0f, pixelRatio);
DrawSquareAtPos(0.0f, 1.0f, pixelRatio);
DrawSquareAtPos(1.0f, 0.0f, pixelRatio);
DrawSquareAtPos(1.0f, 1.0f, pixelRatio);
DrawSquareAtPos(0.5f, 0.0f, pixelRatio);
DrawSquareAtPos(0.0f, 0.5f, pixelRatio);
DrawSquareAtPos(0.5f, 1.0f, pixelRatio);
DrawSquareAtPos(1.0f, 0.5f, pixelRatio);
if (!window->circleFill) {
gs_render_start(true);
float angle = 180;
for (int i = 0, l = 40; i < l; i++) {
gs_vertex2f(sin(RAD(angle)) / 2 + 0.5f, cos(RAD(angle)) / 2 + 0.5f);
angle += 360.0f / (float)l;
gs_vertex2f(sin(RAD(angle)) / 2 + 0.5f, cos(RAD(angle)) / 2 + 0.5f);
gs_vertex2f(0.5f, 1.0f);
}
window->circleFill = gs_render_save();
}
DrawRotationHandle(window->circleFill, obs_sceneitem_get_rot(item) + window->groupRot, pixelRatio);
}
gs_matrix_pop();
GS_DEBUG_MARKER_END();
UNUSED_PARAMETER(scene);
return true;
}
static inline QColor color_from_int(long long val)
{
return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff);
}
QColor CanvasDock::GetSelectionColor() const
{
auto config = get_user_config();
if (config && config_get_bool(config, "Accessibility", "OverrideColors")) {
return color_from_int(config_get_int(config, "Accessibility", "SelectRed"));
}
return QColor::fromRgb(255, 0, 0);
}
QColor CanvasDock::GetCropColor() const
{
auto config = get_user_config();
if (config && config_get_bool(config, "Accessibility", "OverrideColors")) {
return color_from_int(config_get_int(config, "Accessibility", "SelectGreen"));
}
return QColor::fromRgb(0, 255, 0);
}
QColor CanvasDock::GetHoverColor() const
{
auto config = get_user_config();
if (config && config_get_bool(config, "Accessibility", "OverrideColors")) {
return color_from_int(config_get_int(config, "Accessibility", "SelectBlue"));
}
return QColor::fromRgb(0, 127, 255);
}
OBSEventFilter *CanvasDock::BuildEventFilter()
{
return new OBSEventFilter([this](QObject *obj, QEvent *event) {
UNUSED_PARAMETER(obj);
if (!scene) {
return false;
}
switch (event->type()) {
case QEvent::MouseButtonPress:
return this->HandleMousePressEvent(static_cast<QMouseEvent *>(event));
case QEvent::MouseButtonRelease:
return this->HandleMouseReleaseEvent(static_cast<QMouseEvent *>(event));
//case QEvent::MouseButtonDblClick: return this->HandleMouseClickEvent( static_cast<QMouseEvent *>(event));
case QEvent::MouseMove:
return this->HandleMouseMoveEvent(static_cast<QMouseEvent *>(event));
//case QEvent::Enter:
case QEvent::Leave:
return this->HandleMouseLeaveEvent(static_cast<QMouseEvent *>(event));
case QEvent::Wheel:
return this->HandleMouseWheelEvent(static_cast<QWheelEvent *>(event));
//case QEvent::FocusIn:
//case QEvent::FocusOut:
case QEvent::KeyPress:
return this->HandleKeyPressEvent(static_cast<QKeyEvent *>(event));
case QEvent::KeyRelease:
return this->HandleKeyReleaseEvent(static_cast<QKeyEvent *>(event));
default:
return false;
}
});
}
bool CanvasDock::GetSourceRelativeXY(int mouseX, int mouseY, int &relX, int &relY)
{
float pixelRatio = (float)devicePixelRatioF();
int mouseXscaled = (int)roundf((float)mouseX * pixelRatio);
int mouseYscaled = (int)roundf((float)mouseY * pixelRatio);
QSize size = preview->size() * preview->devicePixelRatioF();
obs_source_t *s = obs_weak_source_get_source(source);
uint32_t sourceCX = s ? obs_source_get_width(s) : 1;
if (sourceCX <= 0) {
sourceCX = 1;
}
uint32_t sourceCY = s ? obs_source_get_height(s) : 1;
if (sourceCY <= 0) {
sourceCY = 1;
}
obs_source_release(s);
int x, y;
float scale;
GetScaleAndCenterPos(sourceCX, sourceCY, size.width(), size.height(), x, y, scale);
auto newCX = scale * float(sourceCX);
auto newCY = scale * float(sourceCY);
auto extraCx = /*(zoom - 1.0f) **/ newCX;
auto extraCy = /*(zoom - 1.0f) **/ newCY;
//scale *= zoom;
float scrollX = 0.5f;
float scrollY = 0.5f;
if (x > 0) {
relX = int(((float)mouseXscaled - (float)x + extraCx * scrollX) / scale);
relY = int(((float)mouseYscaled + extraCy * scrollY) / scale);
} else {
relX = int(((float)mouseXscaled + extraCx * scrollX) / scale);
relY = int(((float)mouseYscaled - (float)y + extraCy * scrollY) / scale);
}
// Confirm mouse is inside the source
if (relX < 0 || relX > int(sourceCX)) {
return false;
}
if (relY < 0 || relY > int(sourceCY)) {
return false;
}
return true;
}
bool CanvasDock::HandleMousePressEvent(QMouseEvent *event)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QPointF pos = event->position();
#else
QPointF pos = event->localPos();
#endif
if (scrollMode && IsFixedScaling() && event->button() == Qt::LeftButton) {
setCursor(Qt::ClosedHandCursor);
scrollingFrom.x = (float)pos.x();
scrollingFrom.y = (float)pos.y();
return true;
}
if (event->button() == Qt::RightButton) {
scrollMode = false;
setCursor(Qt::ArrowCursor);
}
if (locked) {
return false;
}
//float pixelRatio = 1.0f;
//float x = pos.x() - main->previewX / pixelRatio;
//float y = pos.y() - main->previewY / pixelRatio;
Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
bool altDown = (modifiers & Qt::AltModifier);
bool shiftDown = (modifiers & Qt::ShiftModifier);
bool ctrlDown = (modifiers & Qt::ControlModifier);
if (event->button() != Qt::LeftButton && event->button() != Qt::RightButton) {
return false;
}
if (event->button() == Qt::LeftButton) {
mouseDown = true;
}
{
std::lock_guard<std::mutex> lock(selectMutex);
selectedItems.clear();
}
if (altDown) {
cropping = true;
}
if (altDown || shiftDown || ctrlDown) {
vec2 s;
SceneFindBoxData sfbd(s, s);
obs_scene_enum_items(scene, FindSelected, &sfbd);
std::lock_guard<std::mutex> lock(selectMutex);
selectedItems = sfbd.sceneItems;
}
startPos = GetMouseEventPos(event);
//vec2_set(&startPos, mouseEvent.x, mouseEvent.y);
//GetStretchHandleData(startPos, false);
//vec2_divf(&startPos, &startPos, main->previewScale / pixelRatio);
startPos.x = std::round(startPos.x);
startPos.y = std::round(startPos.y);
mouseOverItems = SelectedAtPos(scene, startPos);
vec2_zero(&lastMoveOffset);
mousePos = startPos;
return true;
}
static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br)
{
matrix4 boxTransform;
obs_sceneitem_get_box_transform(item, &boxTransform);
vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f);
vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f);
auto GetMinPos = [&](float x, float y) {
vec3 pos;
vec3_set(&pos, x, y, 0.0f);
vec3_transform(&pos, &pos, &boxTransform);
vec3_min(&tl, &tl, &pos);
vec3_max(&br, &br, &pos);
};
GetMinPos(0.0f, 0.0f);
GetMinPos(1.0f, 0.0f);
GetMinPos(0.0f, 1.0f);
GetMinPos(1.0f, 1.0f);
}
static vec3 GetItemTL(obs_sceneitem_t *item)
{
vec3 tl, br;
GetItemBox(item, tl, br);
return tl;
}
static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl)
{
vec3 newTL;
vec2 pos;
obs_sceneitem_get_pos(item, &pos);
newTL = GetItemTL(item);
pos.x += tl.x - newTL.x;
pos.y += tl.y - newTL.y;
obs_sceneitem_set_pos(item, &pos);
}
static bool RotateSelectedSources(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
if (obs_sceneitem_is_group(item)) {
obs_sceneitem_group_enum_items(item, RotateSelectedSources, param);
}
if (!obs_sceneitem_selected(item)) {
return true;
}
if (obs_sceneitem_locked(item)) {
return true;
}
float rot = *reinterpret_cast<float *>(param);
vec3 tl = GetItemTL(item);
rot += obs_sceneitem_get_rot(item);
if (rot >= 360.0f) {
rot -= 360.0f;
} else if (rot <= -360.0f) {
rot += 360.0f;
}
obs_sceneitem_set_rot(item, rot);
obs_sceneitem_force_update_transform(item);
SetItemTL(item, tl);
UNUSED_PARAMETER(scene);
return true;
};
static bool MultiplySelectedItemScale(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
vec2 &mul = *reinterpret_cast<vec2 *>(param);
if (obs_sceneitem_is_group(item)) {
obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param);
}
if (!obs_sceneitem_selected(item)) {
return true;
}
if (obs_sceneitem_locked(item)) {
return true;
}
vec3 tl = GetItemTL(item);
vec2 scale;
obs_sceneitem_get_scale(item, &scale);
vec2_mul(&scale, &scale, &mul);
obs_sceneitem_set_scale(item, &scale);
obs_sceneitem_force_update_transform(item);
SetItemTL(item, tl);
UNUSED_PARAMETER(scene);
return true;
}
static bool CenterAlignSelectedItems(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
obs_bounds_type boundsType = *reinterpret_cast<obs_bounds_type *>(param);
if (obs_sceneitem_is_group(item)) {
obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param);
}
if (!obs_sceneitem_selected(item)) {
return true;
}
if (obs_sceneitem_locked(item)) {
return true;
}
obs_source_t *scene_source = obs_scene_get_source(scene);
obs_transform_info itemInfo;
vec2_set(&itemInfo.pos, 0.0f, 0.0f);
vec2_set(&itemInfo.scale, 1.0f, 1.0f);
itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP;
itemInfo.rot = 0.0f;
vec2_set(&itemInfo.bounds, float(obs_source_get_base_width(scene_source)), float(obs_source_get_base_height(scene_source)));
itemInfo.bounds_type = boundsType;
itemInfo.bounds_alignment = OBS_ALIGN_CENTER;
itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item);
obs_sceneitem_set_info2(item, &itemInfo);
UNUSED_PARAMETER(scene);
return true;
}
static bool GetSelectedItemsWithSize(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
auto items = static_cast<std::vector<obs_sceneitem_t *> *>(param);
if (obs_sceneitem_is_group(item)) {
obs_sceneitem_group_enum_items(item, GetSelectedItemsWithSize, param);
}
if (!obs_sceneitem_selected(item)) {
return true;
}
if (obs_sceneitem_locked(item)) {
return true;
}
vec2 scale;
obs_sceneitem_get_scale(item, &scale);
obs_source_t *source = obs_sceneitem_get_source(item);
const float width = float(obs_source_get_width(source)) * scale.x;
const float height = float(obs_source_get_height(source)) * scale.y;
if (width == 0.0f || height == 0.0f) {
return true;
}
items->push_back(item);
UNUSED_PARAMETER(scene);
return true;
}
void CanvasDock::CenterSelectedItems(CenterType centerType)
{
std::vector<obs_sceneitem_t *> items;
obs_scene_enum_items(scene, GetSelectedItemsWithSize, &items);
if (!items.size()) {
return;
}
// Get center x, y coordinates of items
vec3 center;
float top = M_INFINITE;
float left = M_INFINITE;
float right = 0.0f;
float bottom = 0.0f;
for (auto &item : items) {
vec3 tl, br;
GetItemBox(item, tl, br);
left = (std::min)(tl.x, left);
top = (std::min)(tl.y, top);
right = (std::max)(br.x, right);
bottom = (std::max)(br.y, bottom);
}
center.x = (right + left) / 2.0f;
center.y = (top + bottom) / 2.0f;
center.z = 0.0f;
// Get coordinates of screen center
vec3 screenCenter;
vec3_set(&screenCenter, float(canvas_width), float(canvas_height), 0.0f);
vec3_mulf(&screenCenter, &screenCenter, 0.5f);
// Calculate difference between screen center and item center
vec3 offset;
vec3_sub(&offset, &screenCenter, &center);
// Shift items by offset
for (auto &item : items) {
vec3 tl, br;
GetItemBox(item, tl, br);
vec3_add(&tl, &tl, &offset);
vec3 itemTL = GetItemTL(item);
if (centerType == CenterType::Vertical) {
tl.x = itemTL.x;
} else if (centerType == CenterType::Horizontal) {
tl.y = itemTL.y;
}
SetItemTL(item, tl);
}
}
void CanvasDock::AddSceneItemMenuItems(QMenu *popup, OBSSceneItem sceneItem)
{
popup->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Rename")), [this, sceneItem] {
obs_source_t *item_source = obs_source_get_ref(obs_sceneitem_get_source(sceneItem));
if (!item_source) {
return;
}
obs_canvas_t *canvas = (obs_source_get_output_flags(item_source) & OBS_SOURCE_REQUIRES_CANVAS)
? obs_source_get_canvas(item_source)
: nullptr;
std::string name = obs_source_get_name(item_source);
obs_source_t *s = nullptr;
do {
obs_source_release(s);
if (!NameDialog::AskForName(this, QString::fromUtf8(obs_module_text("SourceName")), name)) {
break;
}
s = canvas ? obs_canvas_get_source_by_name(canvas, name.c_str()) : obs_get_source_by_name(name.c_str());
if (s) {
continue;
}
obs_source_set_name(item_source, name.c_str());
} while (s);
obs_canvas_release(canvas);
obs_source_release(item_source);
});
popup->addAction(
//removeButton->icon(),
QString::fromUtf8(obs_frontend_get_locale_string("Remove")), this, [sceneItem] {
QMessageBox mb(QMessageBox::Question,
QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.Title")),
QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.Text"))
.arg(QString::fromUtf8(obs_source_get_name(obs_sceneitem_get_source(sceneItem)))),
QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No));
mb.setDefaultButton(QMessageBox::NoButton);
if (mb.exec() == QMessageBox::Yes) {
obs_sceneitem_remove(sceneItem);
}
});
popup->addSeparator();
auto orderMenu = popup->addMenu(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Order")));
orderMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Order.MoveUp")), this,
[sceneItem] { obs_sceneitem_set_order(sceneItem, OBS_ORDER_MOVE_UP); });
orderMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Order.MoveDown")), this,
[sceneItem] { obs_sceneitem_set_order(sceneItem, OBS_ORDER_MOVE_DOWN); });
orderMenu->addSeparator();
orderMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Order.MoveToTop")), this,
[sceneItem] { obs_sceneitem_set_order(sceneItem, OBS_ORDER_MOVE_TOP); });
orderMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Order.MoveToBottom")), this,
[sceneItem] { obs_sceneitem_set_order(sceneItem, OBS_ORDER_MOVE_BOTTOM); });
auto transformMenu = popup->addMenu(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform")));
transformMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.EditTransform")),
[this, sceneItem] {
const auto mainDialog = static_cast<QMainWindow *>(obs_frontend_get_main_window());
auto transformDialog = mainDialog->findChild<QDialog *>("OBSBasicTransform");
if (!transformDialog) {
// make sure there is an item selected on the main canvas before starting the transform dialog
const auto currentScene = obs_frontend_preview_program_mode_active()
? obs_frontend_get_current_preview_scene()
: obs_frontend_get_current_scene();
auto selected = GetSelectedItem(obs_scene_from_source(currentScene));
if (!selected) {
obs_scene_enum_items(
obs_scene_from_source(currentScene),
[](obs_scene_t *, obs_sceneitem_t *item, void *) {
obs_sceneitem_select(item, true);
return false;
},
nullptr);
}
obs_source_release(currentScene);
QMetaObject::invokeMethod(mainDialog, "on_actionEditTransform_triggered");
transformDialog = mainDialog->findChild<QDialog *>("OBSBasicTransform");
}
if (!transformDialog) {
return;
}
QMetaObject::invokeMethod(
transformDialog,
obs_get_version() >= MAKE_SEMANTIC_VERSION(32, 1, 0) ? "setItemQt" : "SetItemQt",
Q_ARG(OBSSceneItem, OBSSceneItem(sceneItem)));
});
transformMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.ResetTransform")),
this, [sceneItem] {
obs_sceneitem_set_alignment(sceneItem, OBS_ALIGN_LEFT | OBS_ALIGN_TOP);
obs_sceneitem_set_bounds_type(sceneItem, OBS_BOUNDS_NONE);
vec2 scale;
scale.x = 1.0f;
scale.y = 1.0f;
obs_sceneitem_set_scale(sceneItem, &scale);
vec2 pos;
pos.x = 0.0f;
pos.y = 0.0f;
obs_sceneitem_set_pos(sceneItem, &pos);
obs_sceneitem_crop crop = {0, 0, 0, 0};
obs_sceneitem_set_crop(sceneItem, &crop);
obs_sceneitem_set_rot(sceneItem, 0.0f);
});
transformMenu->addSeparator();
transformMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.Rotate90CW")),
this, [this] {
float rotation = 90.0f;
obs_scene_enum_items(scene, RotateSelectedSources, &rotation);
});
transformMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.Rotate90CCW")),
this, [this] {
float rotation = -90.0f;
obs_scene_enum_items(scene, RotateSelectedSources, &rotation);
});
transformMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.Rotate180")), this,
[this] {
float rotation = 180.0f;
obs_scene_enum_items(scene, RotateSelectedSources, &rotation);
});
transformMenu->addSeparator();
transformMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.FlipHorizontal")),
this, [this] {
vec2 scale;
vec2_set(&scale, -1.0f, 1.0f);
obs_scene_enum_items(scene, MultiplySelectedItemScale, &scale);
});
transformMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.FlipVertical")),
this, [this] {
vec2 scale;
vec2_set(&scale, 1.0f, -1.0f);
obs_scene_enum_items(scene, MultiplySelectedItemScale, &scale);
});
transformMenu->addSeparator();
transformMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.FitToScreen")),
this, [this] {
obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER;
obs_scene_enum_items(scene, CenterAlignSelectedItems, &boundsType);
});
transformMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.StretchToScreen")),
this, [this] {
obs_bounds_type boundsType = OBS_BOUNDS_STRETCH;
obs_scene_enum_items(scene, CenterAlignSelectedItems, &boundsType);
});
transformMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.CenterToScreen")),
this, [this] { CenterSelectedItems(CenterType::Scene); });
transformMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.VerticalCenter")),
this, [this] { CenterSelectedItems(CenterType::Vertical); });
transformMenu->addAction(
QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Transform.HorizontalCenter")), this,
[this] { CenterSelectedItems(CenterType::Horizontal); });
popup->addSeparator();
obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(sceneItem);
auto scaleMenu = popup->addMenu(QString::fromUtf8(obs_frontend_get_locale_string("ScaleFiltering")));
auto a = scaleMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Disable")), this,
[sceneItem] { obs_sceneitem_set_scale_filter(sceneItem, OBS_SCALE_DISABLE); });
a->setCheckable(true);
a->setChecked(scaleFilter == OBS_SCALE_DISABLE);
a = scaleMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("ScaleFiltering.Point")), this,
[sceneItem] { obs_sceneitem_set_scale_filter(sceneItem, OBS_SCALE_POINT); });
a->setCheckable(true);
a->setChecked(scaleFilter == OBS_SCALE_POINT);
a = scaleMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("ScaleFiltering.Bilinear")), this,
[sceneItem] { obs_sceneitem_set_scale_filter(sceneItem, OBS_SCALE_BILINEAR); });
a->setCheckable(true);
a->setChecked(scaleFilter == OBS_SCALE_BILINEAR);
a = scaleMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("ScaleFiltering.Bicubic")), this,
[sceneItem] { obs_sceneitem_set_scale_filter(sceneItem, OBS_SCALE_BICUBIC); });
a->setCheckable(true);
a->setChecked(scaleFilter == OBS_SCALE_BICUBIC);
a = scaleMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("ScaleFiltering.Lanczos")), this,
[sceneItem] { obs_sceneitem_set_scale_filter(sceneItem, OBS_SCALE_LANCZOS); });
a->setCheckable(true);
a->setChecked(scaleFilter == OBS_SCALE_LANCZOS);
a = scaleMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("ScaleFiltering.Area")), this,
[sceneItem] { obs_sceneitem_set_scale_filter(sceneItem, OBS_SCALE_AREA); });
a->setCheckable(true);
a->setChecked(scaleFilter == OBS_SCALE_AREA);
auto blendingMode = obs_sceneitem_get_blending_mode(sceneItem);
auto blendingMenu = popup->addMenu(QString::fromUtf8(obs_frontend_get_locale_string("BlendingMode")));
a = blendingMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("BlendingMode.Normal")), this,
[sceneItem] { obs_sceneitem_set_blending_mode(sceneItem, OBS_BLEND_NORMAL); });
a->setCheckable(true);
a->setChecked(blendingMode == OBS_BLEND_NORMAL);
a = blendingMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("BlendingMode.Additive")), this,
[sceneItem] { obs_sceneitem_set_blending_mode(sceneItem, OBS_BLEND_ADDITIVE); });
a->setCheckable(true);
a->setChecked(blendingMode == OBS_BLEND_ADDITIVE);
a = blendingMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("BlendingMode.Subtract")), this,
[sceneItem] { obs_sceneitem_set_blending_mode(sceneItem, OBS_BLEND_SUBTRACT); });
a->setCheckable(true);
a->setChecked(blendingMode == OBS_BLEND_SUBTRACT);
a = blendingMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("BlendingMode.Screen")), this,
[sceneItem] { obs_sceneitem_set_blending_mode(sceneItem, OBS_BLEND_SCREEN); });
a->setCheckable(true);
a->setChecked(blendingMode == OBS_BLEND_SCREEN);
a = blendingMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("BlendingMode.Multiply")), this,
[sceneItem] { obs_sceneitem_set_blending_mode(sceneItem, OBS_BLEND_MULTIPLY); });
a->setCheckable(true);
a->setChecked(blendingMode == OBS_BLEND_MULTIPLY);
a = blendingMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("BlendingMode.Lighten")), this,
[sceneItem] { obs_sceneitem_set_blending_mode(sceneItem, OBS_BLEND_LIGHTEN); });
a->setCheckable(true);
a->setChecked(blendingMode == OBS_BLEND_LIGHTEN);
a = blendingMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("BlendingMode.Darken")), this,
[sceneItem] { obs_sceneitem_set_blending_mode(sceneItem, OBS_BLEND_DARKEN); });
a->setCheckable(true);
a->setChecked(blendingMode == OBS_BLEND_DARKEN);
popup->addSeparator();
popup->addMenu(CreateVisibilityTransitionMenu(true, sceneItem));
popup->addMenu(CreateVisibilityTransitionMenu(false, sceneItem));
popup->addSeparator();
auto projectorMenu = popup->addMenu(QString::fromUtf8(obs_frontend_get_locale_string("SourceProjector")));
AddProjectorMenuMonitors(projectorMenu, this, SLOT(OpenSourceProjector()));
popup->addAction(QString::fromUtf8(obs_frontend_get_locale_string("SourceWindow")), [sceneItem] {
obs_source_t *s = obs_source_get_ref(obs_sceneitem_get_source(sceneItem));
if (!s) {
return;
}
obs_frontend_open_projector("Source", -1, nullptr, obs_source_get_name(s));
obs_source_release(s);
});
obs_source_t *s = obs_sceneitem_get_source(sceneItem);
popup->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Screenshot.Source")), this,
[s] { obs_frontend_take_source_screenshot(s); });
popup->addSeparator();
popup->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Filters")), this,
[s] { obs_frontend_open_source_filters(s); });
a = popup->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Properties")), this,
[s] { obs_frontend_open_source_properties(s); });
a->setEnabled(obs_source_configurable(s));
}
bool CanvasDock::HandleMouseReleaseEvent(QMouseEvent *event)
{
if (scrollMode) {
setCursor(Qt::OpenHandCursor);
}
if (!mouseDown && event->button() == Qt::RightButton) {
QMenu popup(this);
QAction *a =
popup.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.Preview.Disable")), [this] {
preview_disabled = !preview_disabled;
obs_display_set_enabled(preview->GetDisplay(), !preview_disabled);
preview->setVisible(!preview_disabled);
previewDisabledWidget->setVisible(preview_disabled);
});
auto projectorMenu = popup.addMenu(QString::fromUtf8(obs_frontend_get_locale_string("PreviewProjector")));
AddProjectorMenuMonitors(projectorMenu, this, SLOT(OpenPreviewProjector()));
a = popup.addAction(QString::fromUtf8(obs_frontend_get_locale_string("PreviewWindow")),
[this] { OpenProjector(-1); });
a = popup.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.LockPreview")), this,
[this] { locked = !locked; });
a->setCheckable(true);
a->setChecked(locked);
popup.addAction(GetIconFromType(OBS_ICON_TYPE_IMAGE),
QString::fromUtf8(obs_frontend_get_locale_string("Screenshot")), this, [this] {
auto s = obs_weak_source_get_source(source);
obs_frontend_take_source_screenshot(s);
obs_source_release(s);
});
popup.addMenu(CreateAddSourcePopupMenu());
popup.addSeparator();
OBSSceneItem sceneItem = GetSelectedItem();
if (sceneItem) {
AddSceneItemMenuItems(&popup, sceneItem);
}
popup.exec(QCursor::pos());
return true;
}
if (locked) {
return false;
}
if (!mouseDown) {
return false;
}
const vec2 pos = GetMouseEventPos(event);
if (!mouseMoved) {
ProcessClick(pos);
}
if (selectionBox) {
Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
bool altDown = modifiers & Qt::AltModifier;
bool shiftDown = modifiers & Qt::ShiftModifier;
bool ctrlDown = modifiers & Qt::ControlModifier;
std::lock_guard<std::mutex> lock(selectMutex);
if (altDown || ctrlDown || shiftDown) {
for (size_t i = 0; i < selectedItems.size(); i++) {
obs_sceneitem_select(selectedItems[i], true);
}
}
for (size_t i = 0; i < hoveredPreviewItems.size(); i++) {
bool select = true;
obs_sceneitem_t *item = hoveredPreviewItems[i];
if (altDown) {
select = false;
} else if (ctrlDown) {
select = !obs_sceneitem_selected(item);
}
obs_sceneitem_select(hoveredPreviewItems[i], select);
}
}
if (stretchGroup) {
obs_sceneitem_defer_group_resize_end(stretchGroup);
}
stretchItem = nullptr;
stretchGroup = nullptr;
mouseDown = false;
mouseMoved = false;
cropping = false;
selectionBox = false;
unsetCursor();
OBSSceneItem item = GetItemAtPos(pos, true);
std::lock_guard<std::mutex> lock(selectMutex);
hoveredPreviewItems.clear();
hoveredPreviewItems.push_back(item);
selectedItems.clear();
return true;
}
float CanvasDock::GetDevicePixelRatio()
{
return 1.0f;
}
bool CanvasDock::HandleMouseLeaveEvent(QMouseEvent *event)
{
UNUSED_PARAMETER(event);
std::lock_guard<std::mutex> lock(selectMutex);
if (!selectionBox) {
hoveredPreviewItems.clear();
}
return true;
}
bool CanvasDock::HandleMouseMoveEvent(QMouseEvent *event)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QPointF qtPos = event->position();
#else
QPointF qtPos = event->localPos();
#endif
float pixelRatio = GetDevicePixelRatio();
if (scrollMode && event->buttons() == Qt::LeftButton) {
scrollingOffset.x += pixelRatio * ((float)qtPos.x() - scrollingFrom.x);
scrollingOffset.y += pixelRatio * ((float)qtPos.y() - scrollingFrom.y);
scrollingFrom.x = (float)qtPos.x();
scrollingFrom.y = (float)qtPos.y();
//emit DisplayResized();
return true;
}
if (locked) {
return true;
}
bool updateCursor = false;
if (mouseDown) {
vec2 pos = GetMouseEventPos(event);
if (!mouseMoved && !mouseOverItems && stretchHandle == ItemHandle::None) {
ProcessClick(startPos);
mouseOverItems = SelectedAtPos(scene, startPos);
}
pos.x = std::round(pos.x);
pos.y = std::round(pos.y);
if (stretchHandle != ItemHandle::None) {
if (obs_sceneitem_locked(stretchItem)) {
return true;
}
selectionBox = false;
obs_sceneitem_t *group = obs_sceneitem_get_group(scene, stretchItem);
if (group) {
vec3 group_pos;
vec3_set(&group_pos, pos.x, pos.y, 0.0f);
vec3_transform(&group_pos, &group_pos, &invGroupTransform);
pos.x = group_pos.x;
pos.y = group_pos.y;
}
if (stretchHandle == ItemHandle::Rot) {
RotateItem(pos);
setCursor(Qt::ClosedHandCursor);
} else if (cropping) {
CropItem(pos);
} else {
StretchItem(pos);
}
} else if (mouseOverItems) {
if (cursor().shape() != Qt::SizeAllCursor) {
setCursor(Qt::SizeAllCursor);
}
selectionBox = false;
MoveItems(pos);
} else {
selectionBox = true;
if (!mouseMoved) {
DoSelect(startPos);
}
BoxItems(startPos, pos);
}
mouseMoved = true;
mousePos = pos;
} else {
vec2 pos = GetMouseEventPos(event);
OBSSceneItem item = GetItemAtPos(pos, true);
std::lock_guard<std::mutex> lock(selectMutex);
hoveredPreviewItems.clear();
hoveredPreviewItems.push_back(item);
if (!mouseMoved && hoveredPreviewItems.size() > 0) {
mousePos = pos;
//float scale = GetDevicePixelRatio();
//float x = qtPos.x(); // - main->previewX / scale;
//float y = qtPos.y(); // - main->previewY / scale;
vec2_set(&startPos, pos.x, pos.y);
updateCursor = true;
}
}
if (updateCursor) {
GetStretchHandleData(startPos, true);
uint32_t stretchFlags = (uint32_t)stretchHandle;
UpdateCursor(stretchFlags);
}
return true;
}
bool CanvasDock::HandleMouseWheelEvent(QWheelEvent *event)
{
UNUSED_PARAMETER(event);
return true;
}
bool CanvasDock::HandleKeyPressEvent(QKeyEvent *event)
{
UNUSED_PARAMETER(event);
return true;
}
bool CanvasDock::HandleKeyReleaseEvent(QKeyEvent *event)
{
UNUSED_PARAMETER(event);
return true;
}
static bool CheckItemSelected(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
SceneFindData *data = reinterpret_cast<SceneFindData *>(param);
matrix4 transform;
vec3 transformedPos;
vec3 pos3;
if (!SceneItemHasVideo(item)) {
return true;
}
if (obs_sceneitem_is_group(item)) {
data->group = item;
obs_sceneitem_group_enum_items(item, CheckItemSelected, param);
data->group = nullptr;
if (data->item) {
return false;
}
}
vec3_set(&pos3, data->pos.x, data->pos.y, 0.0f);
obs_sceneitem_get_box_transform(item, &transform);
if (data->group) {
matrix4 parent_transform;
obs_sceneitem_get_draw_transform(data->group, &parent_transform);
matrix4_mul(&transform, &transform, &parent_transform);
}
matrix4_inv(&transform, &transform);
vec3_transform(&transformedPos, &pos3, &transform);
if (transformedPos.x >= 0.0f && transformedPos.x <= 1.0f && transformedPos.y >= 0.0f && transformedPos.y <= 1.0f) {
if (obs_sceneitem_selected(item)) {
data->item = item;
return false;
}
}
UNUSED_PARAMETER(scene);
return true;
}
bool CanvasDock::SelectedAtPos(obs_scene_t *s, const vec2 &pos)
{
if (!s) {
return false;
}
SceneFindData sfd(pos, false);
obs_scene_enum_items(s, CheckItemSelected, &sfd);
return !!sfd.item;
}
static bool select_one(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
obs_sceneitem_t *selectedItem = reinterpret_cast<obs_sceneitem_t *>(param);
if (obs_sceneitem_is_group(item)) {
obs_sceneitem_group_enum_items(item, select_one, param);
}
obs_sceneitem_select(item, (selectedItem == item));
UNUSED_PARAMETER(scene);
return true;
}
void CanvasDock::DoSelect(const vec2 &pos)
{
OBSSceneItem item = GetItemAtPos(pos, true);
obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item);
}
void CanvasDock::DoCtrlSelect(const vec2 &pos)
{
OBSSceneItem item = GetItemAtPos(pos, false);
if (!item) {
return;
}
bool selected = obs_sceneitem_selected(item);
obs_sceneitem_select(item, !selected);
}
void CanvasDock::ProcessClick(const vec2 &pos)
{
Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
if (modifiers & Qt::ControlModifier) {
DoCtrlSelect(pos);
} else {
DoSelect(pos);
}
}
static bool FindItemAtPos(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
SceneFindData *data = reinterpret_cast<SceneFindData *>(param);
matrix4 transform;
matrix4 invTransform;
vec3 transformedPos;
vec3 pos3;
vec3 pos3_;
if (!SceneItemHasVideo(item)) {
return true;
}
if (obs_sceneitem_locked(item)) {
return true;
}
vec3_set(&pos3, data->pos.x, data->pos.y, 0.0f);
obs_sceneitem_get_box_transform(item, &transform);
matrix4_inv(&invTransform, &transform);
vec3_transform(&transformedPos, &pos3, &invTransform);
vec3_transform(&pos3_, &transformedPos, &transform);
if (CloseFloat(pos3.x, pos3_.x) && CloseFloat(pos3.y, pos3_.y) && transformedPos.x >= 0.0f && transformedPos.x <= 1.0f &&
transformedPos.y >= 0.0f && transformedPos.y <= 1.0f) {
if (data->selectBelow && obs_sceneitem_selected(item)) {
if (data->item) {
return false;
} else {
data->selectBelow = false;
}
}
data->item = item;
}
UNUSED_PARAMETER(scene);
return true;
}
OBSSceneItem CanvasDock::GetItemAtPos(const vec2 &pos, bool selectBelow)
{
if (!scene) {
return OBSSceneItem();
}
SceneFindData sfd(pos, selectBelow);
obs_scene_enum_items(scene, FindItemAtPos, &sfd);
return sfd.item;
}
vec2 CanvasDock::GetMouseEventPos(QMouseEvent *event)
{
auto s = obs_weak_source_get_source(source);
uint32_t sourceCX = obs_source_get_width(s);
if (sourceCX <= 0) {
sourceCX = 1;
}
uint32_t sourceCY = obs_source_get_height(s);
if (sourceCY <= 0) {
sourceCY = 1;
}
obs_source_release(s);
int x, y;
float scale;
auto size = preview->size();
GetScaleAndCenterPos(sourceCX, sourceCY, size.width(), size.height(), x, y, scale);
//auto newCX = scale * float(sourceCX);
//auto newCY = scale * float(sourceCY);
float pixelRatio = GetDevicePixelRatio();
QPoint qtPos = event->pos();
vec2 pos;
vec2_set(&pos, ((float)qtPos.x() - (float)x / pixelRatio) / scale, ((float)qtPos.y() - (float)y / pixelRatio) / scale);
return pos;
}
void CanvasDock::UpdateCursor(uint32_t &flags)
{
if (obs_sceneitem_locked(stretchItem)) {
unsetCursor();
return;
}
if (!flags && (cursor().shape() != Qt::OpenHandCursor || !scrollMode)) {
unsetCursor();
}
if (cursor().shape() != Qt::ArrowCursor) {
return;
}
if ((flags & ITEM_LEFT && flags & ITEM_TOP) || (flags & ITEM_RIGHT && flags & ITEM_BOTTOM)) {
setCursor(Qt::SizeFDiagCursor);
} else if ((flags & ITEM_LEFT && flags & ITEM_BOTTOM) || (flags & ITEM_RIGHT && flags & ITEM_TOP)) {
setCursor(Qt::SizeBDiagCursor);
} else if (flags & ITEM_LEFT || flags & ITEM_RIGHT) {
setCursor(Qt::SizeHorCursor);
} else if (flags & ITEM_TOP || flags & ITEM_BOTTOM) {
setCursor(Qt::SizeVerCursor);
} else if (flags & ITEM_ROT) {
setCursor(Qt::OpenHandCursor);
}
}
static void RotatePos(vec2 *pos, float rot)
{
float cosR = cos(rot);
float sinR = sin(rot);
vec2 newPos;
newPos.x = cosR * pos->x - sinR * pos->y;
newPos.y = sinR * pos->x + cosR * pos->y;
vec2_copy(pos, &newPos);
}
void CanvasDock::RotateItem(const vec2 &pos)
{
Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
bool shiftDown = (modifiers & Qt::ShiftModifier);
bool ctrlDown = (modifiers & Qt::ControlModifier);
vec2 pos2;
vec2_copy(&pos2, &pos);
float angle = atan2(pos2.y - rotatePoint.y, pos2.x - rotatePoint.x) + RAD(90);
#define ROT_SNAP(rot, thresh) \
if (abs(angle - RAD((float)rot)) < RAD((float)thresh)) { \
angle = RAD((float)rot); \
}
if (shiftDown) {
for (int i = 0; i <= 360 / 15; i++) {
ROT_SNAP(i * 15 - 90, 7.5);
}
} else if (!ctrlDown) {
ROT_SNAP(rotateAngle, 5)
ROT_SNAP(-90, 5)
ROT_SNAP(-45, 5)
ROT_SNAP(0, 5)
ROT_SNAP(45, 5)
ROT_SNAP(90, 5)
ROT_SNAP(135, 5)
ROT_SNAP(180, 5)
ROT_SNAP(225, 5)
ROT_SNAP(270, 5)
ROT_SNAP(315, 5)
}
#undef ROT_SNAP
vec2 pos3;
vec2_copy(&pos3, &offsetPoint);
RotatePos(&pos3, angle);
pos3.x += rotatePoint.x;
pos3.y += rotatePoint.y;
obs_sceneitem_set_rot(stretchItem, DEG(angle));
obs_sceneitem_set_pos(stretchItem, &pos3);
}
static float maxfunc(float x, float y)
{
return x > y ? x : y;
}
static float minfunc(float x, float y)
{
return x < y ? x : y;
}
void CanvasDock::CropItem(const vec2 &pos)
{
obs_bounds_type boundsType = obs_sceneitem_get_bounds_type(stretchItem);
uint32_t stretchFlags = (uint32_t)stretchHandle;
uint32_t align = obs_sceneitem_get_alignment(stretchItem);
vec3 tl, br, pos3;
vec3_zero(&tl);
vec3_set(&br, stretchItemSize.x, stretchItemSize.y, 0.0f);
vec3_set(&pos3, pos.x, pos.y, 0.0f);
vec3_transform(&pos3, &pos3, &screenToItem);
obs_sceneitem_crop crop = startCrop;
vec2 scale;
obs_sceneitem_get_scale(stretchItem, &scale);
vec2 max_tl;
vec2 max_br;
vec2_set(&max_tl, float(-crop.left) * scale.x, float(-crop.top) * scale.y);
vec2_set(&max_br, stretchItemSize.x + (float)crop.right * scale.x, stretchItemSize.y + (float)crop.bottom * scale.y);
typedef std::function<float(float, float)> minmax_func_t;
minmax_func_t min_x = scale.x < 0.0f ? maxfunc : minfunc;
minmax_func_t min_y = scale.y < 0.0f ? maxfunc : minfunc;
minmax_func_t max_x = scale.x < 0.0f ? minfunc : maxfunc;
minmax_func_t max_y = scale.y < 0.0f ? minfunc : maxfunc;
pos3.x = min_x(pos3.x, max_br.x);
pos3.x = max_x(pos3.x, max_tl.x);
pos3.y = min_y(pos3.y, max_br.y);
pos3.y = max_y(pos3.y, max_tl.y);
if (stretchFlags & ITEM_LEFT) {
float maxX = stretchItemSize.x - (2.0f * scale.x);
pos3.x = tl.x = min_x(pos3.x, maxX);
} else if (stretchFlags & ITEM_RIGHT) {
float minX = (2.0f * scale.x);
pos3.x = br.x = max_x(pos3.x, minX);
}
if (stretchFlags & ITEM_TOP) {
float maxY = stretchItemSize.y - (2.0f * scale.y);
pos3.y = tl.y = min_y(pos3.y, maxY);
} else if (stretchFlags & ITEM_BOTTOM) {
float minY = (2.0f * scale.y);
pos3.y = br.y = max_y(pos3.y, minY);
}
#define ALIGN_X (ITEM_LEFT | ITEM_RIGHT)
#define ALIGN_Y (ITEM_TOP | ITEM_BOTTOM)
vec3 newPos;
vec3_zero(&newPos);
uint32_t align_x = (align & ALIGN_X);
uint32_t align_y = (align & ALIGN_Y);
if (align_x == (stretchFlags & ALIGN_X) && align_x != 0) {
newPos.x = pos3.x;
} else if (align & ITEM_RIGHT) {
newPos.x = stretchItemSize.x;
} else if (!(align & ITEM_LEFT)) {
newPos.x = stretchItemSize.x * 0.5f;
}
if (align_y == (stretchFlags & ALIGN_Y) && align_y != 0) {
newPos.y = pos3.y;
} else if (align & ITEM_BOTTOM) {
newPos.y = stretchItemSize.y;
} else if (!(align & ITEM_TOP)) {
newPos.y = stretchItemSize.y * 0.5f;
}
#undef ALIGN_X
#undef ALIGN_Y
crop = startCrop;
if (stretchFlags & ITEM_LEFT) {
crop.left += int(std::round(tl.x / scale.x));
} else if (stretchFlags & ITEM_RIGHT) {
crop.right += int(std::round((stretchItemSize.x - br.x) / scale.x));
}
if (stretchFlags & ITEM_TOP) {
crop.top += int(std::round(tl.y / scale.y));
} else if (stretchFlags & ITEM_BOTTOM) {
crop.bottom += int(std::round((stretchItemSize.y - br.y) / scale.y));
}
vec3_transform(&newPos, &newPos, &itemToScreen);
newPos.x = std::round(newPos.x);
newPos.y = std::round(newPos.y);
#if 0
vec3 curPos;
vec3_zero(&curPos);
obs_sceneitem_get_pos(stretchItem, (vec2*)&curPos);
blog(LOG_DEBUG, "curPos {%d, %d} - newPos {%d, %d}",
int(curPos.x), int(curPos.y),
int(newPos.x), int(newPos.y));
blog(LOG_DEBUG, "crop {%d, %d, %d, %d}",
crop.left, crop.top,
crop.right, crop.bottom);
#endif
obs_sceneitem_defer_update_begin(stretchItem);
obs_sceneitem_set_crop(stretchItem, &crop);
if (boundsType == OBS_BOUNDS_NONE) {
obs_sceneitem_set_pos(stretchItem, (vec2 *)&newPos);
}
obs_sceneitem_defer_update_end(stretchItem);
}
void CanvasDock::StretchItem(const vec2 &pos)
{
Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
obs_bounds_type boundsType = obs_sceneitem_get_bounds_type(stretchItem);
uint32_t stretchFlags = (uint32_t)stretchHandle;
bool shiftDown = (modifiers & Qt::ShiftModifier);
vec3 tl, br, pos3;
vec3_zero(&tl);
vec3_set(&br, stretchItemSize.x, stretchItemSize.y, 0.0f);
vec3_set(&pos3, pos.x, pos.y, 0.0f);
vec3_transform(&pos3, &pos3, &screenToItem);
if (stretchFlags & ITEM_LEFT) {
tl.x = pos3.x;
} else if (stretchFlags & ITEM_RIGHT) {
br.x = pos3.x;
}
if (stretchFlags & ITEM_TOP) {
tl.y = pos3.y;
} else if (stretchFlags & ITEM_BOTTOM) {
br.y = pos3.y;
}
if (!(modifiers & Qt::ControlModifier)) {
SnapStretchingToScreen(tl, br);
}
obs_source_t *s = obs_sceneitem_get_source(stretchItem);
vec2 baseSize;
vec2_set(&baseSize, float(obs_source_get_width(s)), float(obs_source_get_height(s)));
vec2 size;
vec2_set(&size, br.x - tl.x, br.y - tl.y);
if (boundsType != OBS_BOUNDS_NONE) {
if (shiftDown) {
ClampAspect(tl, br, size, baseSize);
}
if (tl.x > br.x) {
std::swap(tl.x, br.x);
}
if (tl.y > br.y) {
std::swap(tl.y, br.y);
}
vec2_abs(&size, &size);
obs_sceneitem_set_bounds(stretchItem, &size);
} else {
obs_sceneitem_crop crop;
obs_sceneitem_get_crop(stretchItem, &crop);
baseSize.x -= float(crop.left + crop.right);
baseSize.y -= float(crop.top + crop.bottom);
if (baseSize.x > 0.0 && baseSize.y > 0.0) {
if (!shiftDown) {
ClampAspect(tl, br, size, baseSize);
}
vec2_div(&size, &size, &baseSize);
obs_sceneitem_set_scale(stretchItem, &size);
}
}
pos3 = CalculateStretchPos(tl, br);
vec3_transform(&pos3, &pos3, &itemToScreen);
vec2 newPos;
vec2_set(&newPos, std::round(pos3.x), std::round(pos3.y));
obs_sceneitem_set_pos(stretchItem, &newPos);
}
void CanvasDock::SnapStretchingToScreen(vec3 &tl, vec3 &br)
{
uint32_t stretchFlags = (uint32_t)stretchHandle;
vec3 newTL = GetTransformedPos(tl.x, tl.y, itemToScreen);
vec3 newTR = GetTransformedPos(br.x, tl.y, itemToScreen);
vec3 newBL = GetTransformedPos(tl.x, br.y, itemToScreen);
vec3 newBR = GetTransformedPos(br.x, br.y, itemToScreen);
vec3 boundingTL;
vec3 boundingBR;
vec3_copy(&boundingTL, &newTL);
vec3_min(&boundingTL, &boundingTL, &newTR);
vec3_min(&boundingTL, &boundingTL, &newBL);
vec3_min(&boundingTL, &boundingTL, &newBR);
vec3_copy(&boundingBR, &newTL);
vec3_max(&boundingBR, &boundingBR, &newTR);
vec3_max(&boundingBR, &boundingBR, &newBL);
vec3_max(&boundingBR, &boundingBR, &newBR);
vec3 offset = GetSnapOffset(boundingTL, boundingBR);
vec3_add(&offset, &offset, &newTL);
vec3_transform(&offset, &offset, &screenToItem);
vec3_sub(&offset, &offset, &tl);
if (stretchFlags & ITEM_LEFT) {
tl.x += offset.x;
} else if (stretchFlags & ITEM_RIGHT) {
br.x += offset.x;
}
if (stretchFlags & ITEM_TOP) {
tl.y += offset.y;
} else if (stretchFlags & ITEM_BOTTOM) {
br.y += offset.y;
}
}
vec3 CanvasDock::GetSnapOffset(const vec3 &tl, const vec3 &br)
{
auto s = obs_weak_source_get_source(source);
vec2 screenSize;
screenSize.x = (float)obs_source_get_base_width(s);
screenSize.y = (float)obs_source_get_base_height(s);
obs_source_release(s);
vec3 clampOffset;
vec3_zero(&clampOffset);
auto config = get_user_config();
if (!config) {
return clampOffset;
}
const bool snap = config_get_bool(config, "BasicWindow", "SnappingEnabled");
if (snap == false) {
return clampOffset;
}
const bool screenSnap = config_get_bool(config, "BasicWindow", "ScreenSnapping");
const bool centerSnap = config_get_bool(config, "BasicWindow", "CenterSnapping");
const float clampDist = (float)config_get_double(config, "BasicWindow", "SnapDistance") / previewScale;
const float centerX = br.x - (br.x - tl.x) / 2.0f;
const float centerY = br.y - (br.y - tl.y) / 2.0f;
// Left screen edge.
if (screenSnap && fabsf(tl.x) < clampDist) {
clampOffset.x = -tl.x;
}
// Right screen edge.
if (screenSnap && fabsf(clampOffset.x) < EPSILON && fabsf(screenSize.x - br.x) < clampDist) {
clampOffset.x = screenSize.x - br.x;
}
// Horizontal center.
if (centerSnap && fabsf(screenSize.x - (br.x - tl.x)) > clampDist && fabsf(screenSize.x / 2.0f - centerX) < clampDist) {
clampOffset.x = screenSize.x / 2.0f - centerX;
}
// Top screen edge.
if (screenSnap && fabsf(tl.y) < clampDist) {
clampOffset.y = -tl.y;
}
// Bottom screen edge.
if (screenSnap && fabsf(clampOffset.y) < EPSILON && fabsf(screenSize.y - br.y) < clampDist) {
clampOffset.y = screenSize.y - br.y;
}
// Vertical center.
if (centerSnap && fabsf(screenSize.y - (br.y - tl.y)) > clampDist && fabsf(screenSize.y / 2.0f - centerY) < clampDist) {
clampOffset.y = screenSize.y / 2.0f - centerY;
}
return clampOffset;
}
static bool move_items(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
if (obs_sceneitem_locked(item)) {
return true;
}
bool selected = obs_sceneitem_selected(item);
vec2 *offset = reinterpret_cast<vec2 *>(param);
if (obs_sceneitem_is_group(item) && !selected) {
matrix4 transform;
vec3 new_offset;
vec3_set(&new_offset, offset->x, offset->y, 0.0f);
obs_sceneitem_get_draw_transform(item, &transform);
vec4_set(&transform.t, 0.0f, 0.0f, 0.0f, 1.0f);
matrix4_inv(&transform, &transform);
vec3_transform(&new_offset, &new_offset, &transform);
obs_sceneitem_group_enum_items(item, move_items, &new_offset);
}
if (selected) {
vec2 pos;
obs_sceneitem_get_pos(item, &pos);
vec2_add(&pos, &pos, offset);
obs_sceneitem_set_pos(item, &pos);
}
UNUSED_PARAMETER(scene);
return true;
}
void CanvasDock::MoveItems(const vec2 &pos)
{
Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
vec2 offset, moveOffset;
vec2_sub(&offset, &pos, &startPos);
vec2_sub(&moveOffset, &offset, &lastMoveOffset);
if (!(modifiers & Qt::ControlModifier)) {
SnapItemMovement(moveOffset);
}
vec2_add(&lastMoveOffset, &lastMoveOffset, &moveOffset);
obs_scene_enum_items(scene, move_items, &moveOffset);
}
struct SelectedItemBounds {
bool first = true;
vec3 tl, br;
};
static bool AddItemBounds(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
SelectedItemBounds *data = reinterpret_cast<SelectedItemBounds *>(param);
vec3 t[4];
auto add_bounds = [data, &t]() {
for (const vec3 &v : t) {
if (data->first) {
vec3_copy(&data->tl, &v);
vec3_copy(&data->br, &v);
data->first = false;
} else {
vec3_min(&data->tl, &data->tl, &v);
vec3_max(&data->br, &data->br, &v);
}
}
};
if (obs_sceneitem_is_group(item)) {
SelectedItemBounds sib;
obs_sceneitem_group_enum_items(item, AddItemBounds, &sib);
if (!sib.first) {
matrix4 xform;
obs_sceneitem_get_draw_transform(item, &xform);
vec3_set(&t[0], sib.tl.x, sib.tl.y, 0.0f);
vec3_set(&t[1], sib.tl.x, sib.br.y, 0.0f);
vec3_set(&t[2], sib.br.x, sib.tl.y, 0.0f);
vec3_set(&t[3], sib.br.x, sib.br.y, 0.0f);
vec3_transform(&t[0], &t[0], &xform);
vec3_transform(&t[1], &t[1], &xform);
vec3_transform(&t[2], &t[2], &xform);
vec3_transform(&t[3], &t[3], &xform);
add_bounds();
}
}
if (!obs_sceneitem_selected(item)) {
return true;
}
matrix4 boxTransform;
obs_sceneitem_get_box_transform(item, &boxTransform);
t[0] = GetTransformedPos(0.0f, 0.0f, boxTransform);
t[1] = GetTransformedPos(1.0f, 0.0f, boxTransform);
t[2] = GetTransformedPos(0.0f, 1.0f, boxTransform);
t[3] = GetTransformedPos(1.0f, 1.0f, boxTransform);
add_bounds();
UNUSED_PARAMETER(scene);
return true;
}
struct OffsetData {
float clampDist;
vec3 tl, br, offset;
};
static bool GetSourceSnapOffset(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
OffsetData *data = reinterpret_cast<OffsetData *>(param);
if (obs_sceneitem_selected(item)) {
return true;
}
matrix4 boxTransform;
obs_sceneitem_get_box_transform(item, &boxTransform);
vec3 t[4] = {GetTransformedPos(0.0f, 0.0f, boxTransform), GetTransformedPos(1.0f, 0.0f, boxTransform),
GetTransformedPos(0.0f, 1.0f, boxTransform), GetTransformedPos(1.0f, 1.0f, boxTransform)};
bool first = true;
vec3 tl, br;
vec3_zero(&tl);
vec3_zero(&br);
for (const vec3 &v : t) {
if (first) {
vec3_copy(&tl, &v);
vec3_copy(&br, &v);
first = false;
} else {
vec3_min(&tl, &tl, &v);
vec3_max(&br, &br, &v);
}
}
// Snap to other source edges
#define EDGE_SNAP(l, r, x, y) \
do { \
double dist = fabsf(l.x - data->r.x); \
if (dist < data->clampDist && fabsf(data->offset.x) < EPSILON && data->tl.y < br.y && data->br.y > tl.y && \
(fabsf(data->offset.x) > dist || data->offset.x < EPSILON)) \
data->offset.x = l.x - data->r.x; \
} while (false)
EDGE_SNAP(tl, br, x, y);
EDGE_SNAP(tl, br, y, x);
EDGE_SNAP(br, tl, x, y);
EDGE_SNAP(br, tl, y, x);
#undef EDGE_SNAP
UNUSED_PARAMETER(scene);
return true;
}
void CanvasDock::SnapItemMovement(vec2 &offset)
{
SelectedItemBounds sib;
obs_scene_enum_items(scene, AddItemBounds, &sib);
sib.tl.x += offset.x;
sib.tl.y += offset.y;
sib.br.x += offset.x;
sib.br.y += offset.y;
vec3 snapOffset = GetSnapOffset(sib.tl, sib.br);
auto config = get_user_config();
if (!config) {
return;
}
const bool snap = config_get_bool(config, "BasicWindow", "SnappingEnabled");
const bool sourcesSnap = config_get_bool(config, "BasicWindow", "SourceSnapping");
if (snap == false) {
return;
}
if (sourcesSnap == false) {
offset.x += snapOffset.x;
offset.y += snapOffset.y;
return;
}
const float clampDist = (float)config_get_double(config, "BasicWindow", "SnapDistance") / previewScale;
OffsetData offsetData;
offsetData.clampDist = clampDist;
offsetData.tl = sib.tl;
offsetData.br = sib.br;
vec3_copy(&offsetData.offset, &snapOffset);
obs_scene_enum_items(scene, GetSourceSnapOffset, &offsetData);
if (fabsf(offsetData.offset.x) > EPSILON || fabsf(offsetData.offset.y) > EPSILON) {
offset.x += offsetData.offset.x;
offset.y += offsetData.offset.y;
} else {
offset.x += snapOffset.x;
offset.y += snapOffset.y;
}
}
static bool CounterClockwise(float x1, float x2, float x3, float y1, float y2, float y3)
{
return (y3 - y1) * (x2 - x1) > (y2 - y1) * (x3 - x1);
}
static bool IntersectLine(float x1, float x2, float x3, float x4, float y1, float y2, float y3, float y4)
{
bool a = CounterClockwise(x1, x2, x3, y1, y2, y3);
bool b = CounterClockwise(x1, x2, x4, y1, y2, y4);
bool c = CounterClockwise(x3, x4, x1, y3, y4, y1);
bool d = CounterClockwise(x3, x4, x2, y3, y4, y2);
return (a != b) && (c != d);
}
static bool IntersectBox(matrix4 transform, float x1, float x2, float y1, float y2)
{
float x3, x4, y3, y4;
x3 = transform.t.x;
y3 = transform.t.y;
x4 = x3 + transform.x.x;
y4 = y3 + transform.x.y;
if (IntersectLine(x1, x1, x3, x4, y1, y2, y3, y4) || IntersectLine(x1, x2, x3, x4, y1, y1, y3, y4) ||
IntersectLine(x2, x2, x3, x4, y1, y2, y3, y4) || IntersectLine(x1, x2, x3, x4, y2, y2, y3, y4)) {
return true;
}
x4 = x3 + transform.y.x;
y4 = y3 + transform.y.y;
if (IntersectLine(x1, x1, x3, x4, y1, y2, y3, y4) || IntersectLine(x1, x2, x3, x4, y1, y1, y3, y4) ||
IntersectLine(x2, x2, x3, x4, y1, y2, y3, y4) || IntersectLine(x1, x2, x3, x4, y2, y2, y3, y4)) {
return true;
}
x3 = transform.t.x + transform.x.x;
y3 = transform.t.y + transform.x.y;
x4 = x3 + transform.y.x;
y4 = y3 + transform.y.y;
if (IntersectLine(x1, x1, x3, x4, y1, y2, y3, y4) || IntersectLine(x1, x2, x3, x4, y1, y1, y3, y4) ||
IntersectLine(x2, x2, x3, x4, y1, y2, y3, y4) || IntersectLine(x1, x2, x3, x4, y2, y2, y3, y4)) {
return true;
}
x3 = transform.t.x + transform.y.x;
y3 = transform.t.y + transform.y.y;
x4 = x3 + transform.x.x;
y4 = y3 + transform.x.y;
if (IntersectLine(x1, x1, x3, x4, y1, y2, y3, y4) || IntersectLine(x1, x2, x3, x4, y1, y1, y3, y4) ||
IntersectLine(x2, x2, x3, x4, y1, y2, y3, y4) || IntersectLine(x1, x2, x3, x4, y2, y2, y3, y4)) {
return true;
}
return false;
}
static bool FindItemsInBox(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
SceneFindBoxData *data = reinterpret_cast<SceneFindBoxData *>(param);
matrix4 transform;
matrix4 invTransform;
vec3 transformedPos;
vec3 pos3;
vec3 pos3_;
vec2 pos_min, pos_max;
vec2_min(&pos_min, &data->startPos, &data->pos);
vec2_max(&pos_max, &data->startPos, &data->pos);
const float x1 = pos_min.x;
const float x2 = pos_max.x;
const float y1 = pos_min.y;
const float y2 = pos_max.y;
if (!SceneItemHasVideo(item)) {
return true;
}
if (obs_sceneitem_locked(item)) {
return true;
}
if (!obs_sceneitem_visible(item)) {
return true;
}
vec3_set(&pos3, data->pos.x, data->pos.y, 0.0f);
obs_sceneitem_get_box_transform(item, &transform);
matrix4_inv(&invTransform, &transform);
vec3_transform(&transformedPos, &pos3, &invTransform);
vec3_transform(&pos3_, &transformedPos, &transform);
if (CloseFloat(pos3.x, pos3_.x) && CloseFloat(pos3.y, pos3_.y) && transformedPos.x >= 0.0f && transformedPos.x <= 1.0f &&
transformedPos.y >= 0.0f && transformedPos.y <= 1.0f) {
data->sceneItems.push_back(item);
return true;
}
if (transform.t.x > x1 && transform.t.x < x2 && transform.t.y > y1 && transform.t.y < y2) {
data->sceneItems.push_back(item);
return true;
}
if (transform.t.x + transform.x.x > x1 && transform.t.x + transform.x.x < x2 && transform.t.y + transform.x.y > y1 &&
transform.t.y + transform.x.y < y2) {
data->sceneItems.push_back(item);
return true;
}
if (transform.t.x + transform.y.x > x1 && transform.t.x + transform.y.x < x2 && transform.t.y + transform.y.y > y1 &&
transform.t.y + transform.y.y < y2) {
data->sceneItems.push_back(item);
return true;
}
if (transform.t.x + transform.x.x + transform.y.x > x1 && transform.t.x + transform.x.x + transform.y.x < x2 &&
transform.t.y + transform.x.y + transform.y.y > y1 && transform.t.y + transform.x.y + transform.y.y < y2) {
data->sceneItems.push_back(item);
return true;
}
if (transform.t.x + 0.5 * (transform.x.x + transform.y.x) > x1 &&
transform.t.x + 0.5 * (transform.x.x + transform.y.x) < x2 &&
transform.t.y + 0.5 * (transform.x.y + transform.y.y) > y1 &&
transform.t.y + 0.5 * (transform.x.y + transform.y.y) < y2) {
data->sceneItems.push_back(item);
return true;
}
if (IntersectBox(transform, x1, x2, y1, y2)) {
data->sceneItems.push_back(item);
return true;
}
UNUSED_PARAMETER(scene);
return true;
}
void CanvasDock::BoxItems(const vec2 &start_pos, const vec2 &pos)
{
if (!scene) {
return;
}
if (cursor().shape() != Qt::CrossCursor) {
setCursor(Qt::CrossCursor);
}
SceneFindBoxData sfbd(start_pos, pos);
obs_scene_enum_items(scene, FindItemsInBox, &sfbd);
std::lock_guard<std::mutex> lock(selectMutex);
hoveredPreviewItems = sfbd.sceneItems;
}
struct HandleFindData {
const vec2 &pos;
const float radius;
matrix4 parent_xform;
OBSSceneItem item;
ItemHandle handle = ItemHandle::None;
float angle = 0.0f;
vec2 rotatePoint;
vec2 offsetPoint;
float angleOffset = 0.0f;
HandleFindData(const HandleFindData &) = delete;
HandleFindData(HandleFindData &&) = delete;
HandleFindData &operator=(const HandleFindData &) = delete;
HandleFindData &operator=(HandleFindData &&) = delete;
inline HandleFindData(const vec2 &pos_, float scale) : pos(pos_), radius(HANDLE_SEL_RADIUS / scale)
{
matrix4_identity(&parent_xform);
}
inline HandleFindData(const HandleFindData &hfd, obs_sceneitem_t *parent)
: pos(hfd.pos),
radius(hfd.radius),
item(hfd.item),
handle(hfd.handle),
angle(hfd.angle),
rotatePoint(hfd.rotatePoint),
offsetPoint(hfd.offsetPoint)
{
obs_sceneitem_get_draw_transform(parent, &parent_xform);
}
};
static bool FindHandleAtPos(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
HandleFindData &data = *reinterpret_cast<HandleFindData *>(param);
if (!obs_sceneitem_selected(item)) {
if (obs_sceneitem_is_group(item)) {
HandleFindData newData(data, item);
newData.angleOffset = obs_sceneitem_get_rot(item);
obs_sceneitem_group_enum_items(item, FindHandleAtPos, &newData);
data.item = newData.item;
data.handle = newData.handle;
data.angle = newData.angle;
data.rotatePoint = newData.rotatePoint;
data.offsetPoint = newData.offsetPoint;
}
return true;
}
matrix4 transform;
vec3 pos3;
float closestHandle = data.radius;
vec3_set(&pos3, data.pos.x, data.pos.y, 0.0f);
obs_sceneitem_get_box_transform(item, &transform);
auto TestHandle = [&](float x, float y, ItemHandle handle) {
vec3 handlePos = GetTransformedPos(x, y, transform);
vec3_transform(&handlePos, &handlePos, &data.parent_xform);
float dist = vec3_dist(&handlePos, &pos3);
if (dist < data.radius) {
if (dist < closestHandle) {
closestHandle = dist;
data.handle = handle;
data.item = item;
}
}
};
TestHandle(0.0f, 0.0f, ItemHandle::TopLeft);
TestHandle(0.5f, 0.0f, ItemHandle::TopCenter);
TestHandle(1.0f, 0.0f, ItemHandle::TopRight);
TestHandle(0.0f, 0.5f, ItemHandle::CenterLeft);
TestHandle(1.0f, 0.5f, ItemHandle::CenterRight);
TestHandle(0.0f, 1.0f, ItemHandle::BottomLeft);
TestHandle(0.5f, 1.0f, ItemHandle::BottomCenter);
TestHandle(1.0f, 1.0f, ItemHandle::BottomRight);
vec2 rotHandleOffset;
vec2_set(&rotHandleOffset, 0.0f, HANDLE_RADIUS * data.radius * 1.5f - data.radius);
RotatePos(&rotHandleOffset, atan2(transform.x.y, transform.x.x));
RotatePos(&rotHandleOffset, RAD(data.angleOffset));
vec3 handlePos = GetTransformedPos(0.5f, 0.0f, transform);
vec3_transform(&handlePos, &handlePos, &data.parent_xform);
handlePos.x -= rotHandleOffset.x;
handlePos.y -= rotHandleOffset.y;
float dist = vec3_dist(&handlePos, &pos3);
if (dist < data.radius) {
if (dist < closestHandle) {
closestHandle = dist;
data.item = item;
data.angle = obs_sceneitem_get_rot(item);
data.handle = ItemHandle::Rot;
vec2_set(&data.rotatePoint, transform.t.x + transform.x.x / 2 + transform.y.x / 2,
transform.t.y + transform.x.y / 2 + transform.y.y / 2);
obs_sceneitem_get_pos(item, &data.offsetPoint);
data.offsetPoint.x -= data.rotatePoint.x;
data.offsetPoint.y -= data.rotatePoint.y;
RotatePos(&data.offsetPoint, -RAD(obs_sceneitem_get_rot(item)));
}
}
UNUSED_PARAMETER(scene);
return true;
}
void CanvasDock::GetStretchHandleData(const vec2 &pos, bool ignoreGroup)
{
if (!scene) {
return;
}
HandleFindData hfd(pos, previewScale);
obs_scene_enum_items(scene, FindHandleAtPos, &hfd);
stretchItem = std::move(hfd.item);
stretchHandle = hfd.handle;
rotateAngle = hfd.angle;
rotatePoint = hfd.rotatePoint;
offsetPoint = hfd.offsetPoint;
if (stretchHandle != ItemHandle::None) {
matrix4 boxTransform;
vec3 itemUL;
float itemRot;
stretchItemSize = GetItemSize(stretchItem);
obs_sceneitem_get_box_transform(stretchItem, &boxTransform);
itemRot = obs_sceneitem_get_rot(stretchItem);
vec3_from_vec4(&itemUL, &boxTransform.t);
/* build the item space conversion matrices */
matrix4_identity(&itemToScreen);
matrix4_rotate_aa4f(&itemToScreen, &itemToScreen, 0.0f, 0.0f, 1.0f, RAD(itemRot));
matrix4_translate3f(&itemToScreen, &itemToScreen, itemUL.x, itemUL.y, 0.0f);
matrix4_identity(&screenToItem);
matrix4_translate3f(&screenToItem, &screenToItem, -itemUL.x, -itemUL.y, 0.0f);
matrix4_rotate_aa4f(&screenToItem, &screenToItem, 0.0f, 0.0f, 1.0f, RAD(-itemRot));
obs_sceneitem_get_crop(stretchItem, &startCrop);
obs_sceneitem_get_pos(stretchItem, &startItemPos);
obs_source_t *s = obs_sceneitem_get_source(stretchItem);
cropSize.x = float(obs_source_get_width(s) - startCrop.left - startCrop.right);
cropSize.y = float(obs_source_get_height(s) - startCrop.top - startCrop.bottom);
stretchGroup = obs_sceneitem_get_group(scene, stretchItem);
if (stretchGroup && !ignoreGroup) {
obs_sceneitem_get_draw_transform(stretchGroup, &invGroupTransform);
matrix4_inv(&invGroupTransform, &invGroupTransform);
obs_sceneitem_defer_group_resize_begin(stretchGroup);
} else {
stretchGroup = nullptr;
}
}
}
void CanvasDock::ClampAspect(vec3 &tl, vec3 &br, vec2 &size, const vec2 &baseSize)
{
float baseAspect = baseSize.x / baseSize.y;
float aspect = size.x / size.y;
uint32_t stretchFlags = (uint32_t)stretchHandle;
if (stretchHandle == ItemHandle::TopLeft || stretchHandle == ItemHandle::TopRight ||
stretchHandle == ItemHandle::BottomLeft || stretchHandle == ItemHandle::BottomRight) {
if (aspect < baseAspect) {
if ((size.y >= 0.0f && size.x >= 0.0f) || (size.y <= 0.0f && size.x <= 0.0f)) {
size.x = size.y * baseAspect;
} else {
size.x = size.y * baseAspect * -1.0f;
}
} else {
if ((size.y >= 0.0f && size.x >= 0.0f) || (size.y <= 0.0f && size.x <= 0.0f)) {
size.y = size.x / baseAspect;
} else {
size.y = size.x / baseAspect * -1.0f;
}
}
} else if (stretchHandle == ItemHandle::TopCenter || stretchHandle == ItemHandle::BottomCenter) {
if ((size.y >= 0.0f && size.x >= 0.0f) || (size.y <= 0.0f && size.x <= 0.0f)) {
size.x = size.y * baseAspect;
} else {
size.x = size.y * baseAspect * -1.0f;
}
} else if (stretchHandle == ItemHandle::CenterLeft || stretchHandle == ItemHandle::CenterRight) {
if ((size.y >= 0.0f && size.x >= 0.0f) || (size.y <= 0.0f && size.x <= 0.0f)) {
size.y = size.x / baseAspect;
} else {
size.y = size.x / baseAspect * -1.0f;
}
}
size.x = std::round(size.x);
size.y = std::round(size.y);
if (stretchFlags & ITEM_LEFT) {
tl.x = br.x - size.x;
} else if (stretchFlags & ITEM_RIGHT) {
br.x = tl.x + size.x;
}
if (stretchFlags & ITEM_TOP) {
tl.y = br.y - size.y;
} else if (stretchFlags & ITEM_BOTTOM) {
br.y = tl.y + size.y;
}
}
vec3 CanvasDock::CalculateStretchPos(const vec3 &tl, const vec3 &br)
{
uint32_t alignment = obs_sceneitem_get_alignment(stretchItem);
vec3 pos;
vec3_zero(&pos);
if (alignment & OBS_ALIGN_LEFT) {
pos.x = tl.x;
} else if (alignment & OBS_ALIGN_RIGHT) {
pos.x = br.x;
} else {
pos.x = (br.x - tl.x) * 0.5f + tl.x;
}
if (alignment & OBS_ALIGN_TOP) {
pos.y = tl.y;
} else if (alignment & OBS_ALIGN_BOTTOM) {
pos.y = br.y;
} else {
pos.y = (br.y - tl.y) * 0.5f + tl.y;
}
return pos;
}
bool CanvasDock::DrawSelectionBox(float x1, float y1, float x2, float y2, gs_vertbuffer_t *rect_fill)
{
float pixelRatio = GetDevicePixelRatio();
x1 = std::round(x1);
x2 = std::round(x2);
y1 = std::round(y1);
y2 = std::round(y2);
gs_effect_t *eff = gs_get_effect();
gs_eparam_t *colParam = gs_effect_get_param_by_name(eff, "color");
vec4 fillColor;
vec4_set(&fillColor, 0.7f, 0.7f, 0.7f, 0.5f);
vec4 borderColor;
vec4_set(&borderColor, 1.0f, 1.0f, 1.0f, 1.0f);
vec2 scale;
vec2_set(&scale, std::abs(x2 - x1), std::abs(y2 - y1));
gs_matrix_push();
gs_matrix_identity();
gs_matrix_translate3f(x1, y1, 0.0f);
gs_matrix_scale3f(x2 - x1, y2 - y1, 1.0f);
gs_effect_set_vec4(colParam, &fillColor);
gs_load_vertexbuffer(rect_fill);
gs_draw(GS_TRISTRIP, 0, 0);
gs_effect_set_vec4(colParam, &borderColor);
DrawRect(HANDLE_RADIUS * pixelRatio / 2, scale);
gs_matrix_pop();
return true;
}
struct descendant_info {
bool exists;
obs_weak_source_t *target;
obs_source_t *target2;
};
static void check_descendant(obs_source_t *parent, obs_source_t *child, void *param)
{
auto *info = (struct descendant_info *)param;
if (parent == info->target2 || child == info->target2 || obs_weak_source_references_source(info->target, child) ||
obs_weak_source_references_source(info->target, parent)) {
info->exists = true;
}
}
bool CanvasDock::add_sources_of_type_to_menu(void *param, obs_source_t *source)
{
QMenu *menu = static_cast<QMenu *>(param);
auto parent = qobject_cast<QMenu *>(menu->parent());
auto a = parent && !parent->menuAction()->data().isNull() ? parent->menuAction() : menu->menuAction();
while (parent && qobject_cast<QMenu *>(parent->parent())) {
parent = qobject_cast<QMenu *>(parent->parent());
}
CanvasDock *cd = static_cast<CanvasDock *>(parent ? parent->parent() : menu->parent());
auto t = a->data().toString();
auto idUtf8 = t.toUtf8();
const char *id = idUtf8.constData();
if (strcmp(obs_source_get_unversioned_id(source), id) == 0) {
auto name = QString::fromUtf8(obs_source_get_name(source));
QList<QAction *> actions = menu->actions();
QAction *after = nullptr;
for (QAction *menuAction : actions) {
if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) {
after = menuAction;
break;
}
}
auto na = new QAction(name, menu);
connect(na, &QAction::triggered, cd, [cd, source] { cd->AddSourceToScene(source); }, Qt::QueuedConnection);
menu->insertAction(after, na);
struct descendant_info info = {false, cd->source, obs_scene_get_source(cd->scene)};
obs_source_enum_full_tree(source, check_descendant, &info);
na->setEnabled(!info.exists);
}
return true;
}
void CanvasDock::LoadSourceTypeMenu(QMenu *menu, const char *type)
{
menu->clear();
if (obs_get_source_output_flags(type) & OBS_SOURCE_REQUIRES_CANVAS) {
obs_enum_canvases(
[](void *param, obs_canvas_t *canvas) {
QMenu *m = (QMenu *)param;
auto canvas_name = QString::fromUtf8(obs_canvas_get_name(canvas));
auto cm = m->addMenu(canvas_name);
obs_canvas_enum_scenes(canvas, add_sources_of_type_to_menu, cm);
return true;
},
menu);
} else if (strcmp(type, "scene") == 0) {
obs_enum_scenes(add_sources_of_type_to_menu, menu);
} else {
obs_enum_sources(add_sources_of_type_to_menu, menu);
auto popupItem = new QAction(QString::fromUtf8(obs_frontend_get_locale_string("New")), menu);
popupItem->setData(QString::fromUtf8(type));
connect(popupItem, SIGNAL(triggered(bool)), this, SLOT(AddSourceFromAction()));
QList<QAction *> actions = menu->actions();
QAction *first = actions.size() ? actions.first() : nullptr;
menu->insertAction(first, popupItem);
menu->insertSeparator(first);
}
}
void CanvasDock::AddSourceToScene(obs_source_t *s)
{
obs_scene_add(scene, s);
}
void CanvasDock::AddSourceTypeToMenu(QMenu *popup, const char *source_type, const char *name)
{
QString qname = QString::fromUtf8(name);
QAction *popupItem = new QAction(qname, popup);
if (strcmp(source_type, "scene") == 0) {
popupItem->setIcon(GetSceneIcon());
} else if (strcmp(source_type, "group") == 0) {
popupItem->setIcon(GetGroupIcon());
} else {
popupItem->setIcon(GetIconFromType(obs_source_get_icon_type(source_type)));
}
popupItem->setData(QString::fromUtf8(source_type));
QMenu *menu = new QMenu(popup);
popupItem->setMenu(menu);
QObject::connect(menu, &QMenu::aboutToShow, [this, menu, source_type] { LoadSourceTypeMenu(menu, source_type); });
QList<QAction *> actions = popup->actions();
QAction *after = nullptr;
for (QAction *menuAction : actions) {
if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) {
after = menuAction;
break;
}
}
popup->insertAction(after, popupItem);
}
QMenu *CanvasDock::CreateAddSourcePopupMenu()
{
const char *unversioned_type;
const char *type;
bool foundValues = false;
bool foundDeprecated = false;
size_t idx = 0;
QMenu *popup = new QMenu(QString::fromUtf8(obs_frontend_get_locale_string("Add")), this);
QMenu *deprecated = new QMenu(QString::fromUtf8(obs_frontend_get_locale_string("Deprecated")), popup);
while (obs_enum_input_types2(idx++, &type, &unversioned_type)) {
const char *name = obs_source_get_display_name(type);
if (!name) {
continue;
}
uint32_t caps = obs_get_source_output_flags(type);
if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) {
continue;
}
if ((caps & OBS_SOURCE_DEPRECATED) == 0) {
AddSourceTypeToMenu(popup, unversioned_type, name);
} else {
AddSourceTypeToMenu(deprecated, unversioned_type, name);
foundDeprecated = true;
}
foundValues = true;
}
AddSourceTypeToMenu(popup, "scene", obs_frontend_get_locale_string("Basic.Scene"));
AddSourceTypeToMenu(popup, "group", obs_frontend_get_locale_string("Group"));
if (!foundDeprecated) {
delete deprecated;
deprecated = nullptr;
}
if (!foundValues) {
delete popup;
popup = nullptr;
} else if (foundDeprecated) {
popup->addSeparator();
popup->addMenu(deprecated);
}
return popup;
}
void CanvasDock::AddSourceFromAction()
{
QAction *a = qobject_cast<QAction *>(sender());
if (!a) {
return;
}
auto t = a->data().toString();
auto idUtf8 = t.toUtf8();
const char *id = idUtf8.constData();
if (id && *id && strlen(id)) {
const char *v_id = obs_get_latest_input_type_id(id);
QString placeHolderText = QString::fromUtf8(obs_source_get_display_name(v_id));
QString text = placeHolderText;
int i = 2;
OBSSourceAutoRelease s = nullptr;
obs_source_t *created_source = nullptr;
if (obs_get_source_output_flags(id) & OBS_SOURCE_REQUIRES_CANVAS) {
while ((s = obs_canvas_get_source_by_name(canvas, text.toUtf8().constData()))) {
text = QString("%1 %2").arg(placeHolderText).arg(i++);
}
created_source = obs_scene_get_source(obs_canvas_scene_create(canvas, text.toUtf8().constData()));
} else {
while ((s = obs_get_source_by_name(text.toUtf8().constData()))) {
text = QString("%1 %2").arg(placeHolderText).arg(i++);
}
created_source = obs_source_create(v_id, text.toUtf8().constData(), nullptr, nullptr);
}
obs_scene_add(scene, created_source);
if (obs_source_configurable(created_source)) {
obs_frontend_open_source_properties(created_source);
}
obs_source_release(created_source);
}
}
bool CanvasDock::StartVideo()
{
obs_canvas_t *c = nullptr;
obs_frontend_canvas_list cl = {};
obs_frontend_get_canvases(&cl);
for (size_t i = 0; i < cl.canvases.num; i++) {
if (strcmp(obs_canvas_get_name(cl.canvases.array[i]), CANVAS_NAME) == 0 &&
!obs_canvas_removed(cl.canvases.array[i])) {
c = obs_canvas_get_ref(cl.canvases.array[i]);
break;
}
}
obs_frontend_canvas_list_free(&cl);
if (canvas) {
obs_canvas_release(canvas);
}
canvas = c ? c : obs_frontend_add_canvas(CANVAS_NAME, nullptr, PROGRAM);
auto ph = obs_get_proc_handler();
calldata_t cd2 = {0};
calldata_set_ptr(&cd2, "canvas", canvas);
calldata_set_string(&cd2, "canvas_name", CANVAS_NAME);
calldata_set_ptr(&cd2, "get_transitions", (void *)CanvasDock::get_transitions);
calldata_set_ptr(&cd2, "get_transitions_data", this);
proc_handler_call(ph, "downstream_keyer_add_canvas", &cd2);
calldata_free(&cd2);
//auto sh = obs_canvas_get_signal_handler(canvas);
//signal_handler_connect(sh, "source_rename", source_rename, this);
//signal_handler_connect(sh, "source_remove", source_remove, this);
auto s = obs_weak_source_get_source(source);
obs_canvas_set_channel(canvas, 0, s);
obs_source_release(s);
bool started_video = false;
if (!obs_canvas_has_video(canvas)) {
obs_video_info ovi;
obs_get_video_info(&ovi);
ovi.base_width = canvas_width;
ovi.base_height = canvas_height;
ovi.output_width = canvas_width;
ovi.output_height = canvas_height;
started_video = obs_canvas_reset_video(canvas, &ovi);
}
return started_video;
}
void CanvasDock::virtual_cam_output_start(void *data, calldata_t *calldata)
{
UNUSED_PARAMETER(calldata);
auto d = static_cast<CanvasDock *>(data);
d->SendVendorEvent("virtual_camera_started");
d->CheckReplayBuffer(true);
QMetaObject::invokeMethod(d, "OnVirtualCamStart");
}
void CanvasDock::virtual_cam_output_stop(void *data, calldata_t *calldata)
{
UNUSED_PARAMETER(calldata);
auto d = static_cast<CanvasDock *>(data);
d->SendVendorEvent("virtual_camera_stopped");
QMetaObject::invokeMethod(d, "OnVirtualCamStop");
signal_handler_t *signal = obs_output_get_signal_handler(d->virtualCamOutput);
signal_handler_disconnect(signal, "start", virtual_cam_output_start, d);
signal_handler_disconnect(signal, "stop", virtual_cam_output_stop, d);
obs_output_release(d->virtualCamOutput);
d->virtualCamOutput = nullptr;
}
void CanvasDock::OnVirtualCamStart()
{
virtualCamButton->setIcon(virtualCamActiveIcon);
virtualCamButton->setChecked(true);
}
void CanvasDock::OnVirtualCamStop()
{
virtualCamButton->setIcon(virtualCamInactiveIcon);
virtualCamButton->setChecked(false);
CheckReplayBuffer();
if (multiCanvasSource) {
multi_canvas_source_remove_canvas(obs_obj_get_data(multiCanvasSource), canvas);
obs_source_release(multiCanvasSource);
multiCanvasSource = nullptr;
}
if (multiCanvasVideo) {
multiCanvasVideo = nullptr;
obs_canvas_set_channel(multiCanvas, 0, nullptr);
}
if (multiCanvas) {
obs_canvas_release(multiCanvas);
multiCanvas = nullptr;
}
}
void CanvasDock::VirtualCamButtonClicked()
{
if (obs_output_active(virtualCamOutput)) {
StopVirtualCam();
} else {
StartVirtualCam();
}
}
void CanvasDock::StartVirtualCam()
{
CheckReplayBuffer(true);
const auto output = obs_frontend_get_virtualcam_output();
if (obs_output_active(output)) {
if (!virtualCamOutput) {
virtualCamButton->setChecked(false);
}
obs_output_release(output);
return;
}
if (!virtual_cam_warned && isVisible()) {
QMessageBox::warning(this, QString::fromUtf8(obs_module_text("VirtualCameraVertical")),
QString::fromUtf8(obs_module_text("VirtualCameraWarning")));
virtual_cam_warned = true;
}
virtualCamOutput = output;
obs_canvas_t *started_canvas = nullptr;
bool started_video = false;
video_t *virtual_video = nullptr;
if (virtual_cam_mode == VIRTUAL_CAMERA_VERTICAL) {
started_video = StartVideo();
started_canvas = canvas;
virtual_video = obs_canvas_get_video(canvas);
} else if (virtual_cam_mode == VIRTUAL_CAMERA_BOTH) {
if (multiCanvas && obs_canvas_removed(multiCanvas)) {
obs_canvas_release(multiCanvas);
multiCanvas = nullptr;
}
if (!multiCanvas) {
multiCanvas = obs_get_canvas_by_name("multiCanvas");
if (multiCanvas && obs_canvas_removed(multiCanvas)) {
obs_canvas_release(multiCanvas);
multiCanvas = nullptr;
}
if (!multiCanvas) {
multiCanvas = obs_canvas_create("multiCanvas", nullptr, DEVICE);
}
}
started_canvas = multiCanvas;
if (!multiCanvasSource) {
multiCanvasSource =
obs_source_create_private("vertical_multi_canvas_source", "vertical_multi_canvas_source", nullptr);
void *view_data = obs_obj_get_data(multiCanvasSource);
multi_canvas_source_add_canvas(view_data, canvas, canvas_width, canvas_height);
}
if (!multiCanvasVideo) {
auto w = obs_source_get_width(multiCanvasSource);
auto h = obs_source_get_height(multiCanvasSource);
obs_video_info ovi;
if (!obs_canvas_get_video_info(multiCanvas, &ovi) || ovi.base_width != w || ovi.base_height != h ||
ovi.output_width != w || ovi.output_height != h) {
obs_get_video_info(&ovi);
ovi.base_width = w;
ovi.base_height = h;
ovi.output_width = w;
ovi.output_height = h;
obs_canvas_reset_video(multiCanvas, &ovi);
}
multiCanvasVideo = obs_canvas_get_video(multiCanvas);
started_video = true;
}
virtual_video = multiCanvasVideo;
if (obs_canvas_get_channel(multiCanvas, 0) != multiCanvasSource) {
obs_canvas_set_channel(multiCanvas, 0, multiCanvasSource);
}
} else {
virtual_video = obs_get_video();
}
signal_handler_t *signal = obs_output_get_signal_handler(output);
signal_handler_disconnect(signal, "start", virtual_cam_output_start, this);
signal_handler_disconnect(signal, "stop", virtual_cam_output_stop, this);
signal_handler_connect(signal, "start", virtual_cam_output_start, this);
signal_handler_connect(signal, "stop", virtual_cam_output_stop, this);
obs_output_set_media(output, virtual_video, obs_get_audio());
SendVendorEvent("virtual_camera_starting");
const bool success = obs_output_start(output);
if (!success) {
QMetaObject::invokeMethod(this, "OnVirtualCamStop");
if (started_video) {
if (obs_canvas_get_video(canvas) == virtual_video) {
DestroyVideo();
} else if (multiCanvasVideo == virtual_video) {
multiCanvasVideo = nullptr;
obs_canvas_set_channel(started_canvas, 0, nullptr);
obs_canvas_release(started_canvas);
multiCanvas = nullptr;
} else {
obs_canvas_set_channel(started_canvas, 0, nullptr);
}
}
}
}
void CanvasDock::StopVirtualCam()
{
if (!obs_output_active(virtualCamOutput)) {
virtualCamButton->setChecked(false);
return;
}
SendVendorEvent("virtual_camera_stopping");
if (obs_output_video(virtualCamOutput) != obs_get_video()) {
obs_output_set_media(virtualCamOutput, nullptr, nullptr);
}
obs_output_stop(virtualCamOutput);
}
void CanvasDock::ConfigButtonClicked()
{
if (!configDialog) {
configDialog = new OBSBasicSettings(this, (QMainWindow *)obs_frontend_get_main_window());
}
configDialog->LoadSettings();
configDialog->exec();
save_canvas();
}
void CanvasDock::ReplayButtonClicked(QString filename)
{
if (!obs_output_active(replayOutput)) {
if (replayButton->isChecked()) {
replayButton->setChecked(false);
}
return;
}
if (!replayButton->isChecked()) {
replayButton->setChecked(true);
}
obs_data_t *s = obs_output_get_settings(replayOutput);
if (!filename.isEmpty()) {
if (replayFilename.empty()) {
replayFilename = obs_data_get_string(s, "format");
}
obs_data_set_string(s, "format", filename.toUtf8().constData());
} else if (!replayFilename.empty()) {
obs_data_set_string(s, "format", replayFilename.c_str());
}
obs_data_release(s);
calldata_t cd = {0};
proc_handler_t *ph = obs_output_get_proc_handler(replayOutput);
proc_handler_call(ph, "save", &cd);
calldata_free(&cd);
statusLabel->setText(QString::fromUtf8(obs_module_text("Saving")));
replayStatusResetTimer.start(10000);
SendVendorEvent("backtrack_saving");
}
int GetConfigPath(char *path, size_t size, const char *name)
{
#if ALLOW_PORTABLE_MODE
if (portable_mode) {
if (name && *name) {
return snprintf(path, size, CONFIG_PATH "/%s", name);
} else {
return snprintf(path, size, CONFIG_PATH);
}
} else {
return os_get_config_path(path, size, name);
}
#else
return os_get_config_path(path, size, name);
#endif
}
static inline int GetProfilePath(char *path, size_t size, const char *file)
{
char profiles_path[512];
auto config = get_user_config();
if (!config) {
return -1;
}
const char *profile = config_get_string(config, "Basic", "ProfileDir");
if (!profile) {
return -1;
}
if (!path) {
return -1;
}
if (!file) {
file = "";
}
int ret = GetConfigPath(profiles_path, 512, "obs-studio/basic/profiles");
if (ret <= 0) {
return ret;
}
if (!*file) {
return snprintf(path, size, "%s/%s", profiles_path, profile);
}
return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file);
}
static obs_data_t *GetDataFromJsonFile(const char *jsonFile)
{
char fullPath[512];
obs_data_t *data = nullptr;
int ret = GetProfilePath(fullPath, sizeof(fullPath), jsonFile);
if (ret > 0) {
BPtr<char> jsonData = os_quick_read_utf8_file(fullPath);
if (!!jsonData) {
data = obs_data_create_from_json(jsonData);
}
}
if (!data) {
data = obs_data_create();
}
return data;
}
void CanvasDock::RecordButtonClicked()
{
if (obs_output_active(recordOutput)) {
StopRecord();
} else {
StartRecord();
}
}
void CanvasDock::StartRecord()
{
if (obs_output_active(recordOutput)) {
return;
}
const char *format = nullptr;
config_t *config = obs_frontend_get_profile_config();
const char *mode = config_get_string(config, "Output", "Mode");
const char *dir = nullptr;
bool ffmpegOutput = false;
if (record_advanced_settings) {
if (file_format.empty()) {
file_format = "mkv";
}
format = file_format.c_str();
dir = recordPath.c_str();
} else if (strcmp(mode, "Advanced") == 0) {
const char *recType = config_get_string(config, "AdvOut", "RecType");
if (strcmp(recType, "FFmpeg") == 0) {
ffmpegOutput = true;
dir = config_get_string(config, "AdvOut", "FFFilePath");
} else {
dir = config_get_string(config, "AdvOut", "RecFilePath");
}
bool ffmpegRecording = ffmpegOutput && config_get_bool(config, "AdvOut", "FFOutputToFile");
if (ffmpegRecording) {
format = config_get_string(config, "AdvOut", "FFExtension");
} else if (!config_has_user_value(config, "AdvOut", "RecFormat2") &&
config_has_user_value(config, "AdvOut", "RecFormat")) {
format = config_get_string(config, "AdvOut", "RecFormat");
} else {
format = config_get_string(config, "AdvOut", "RecFormat2");
}
} else {
dir = config_get_string(config, "SimpleOutput", "FilePath");
if (!config_has_user_value(config, "SimpleOutput", "RecFormat2") &&
config_has_user_value(config, "SimpleOutput", "RecFormat")) {
format = config_get_string(config, "SimpleOutput", "RecFormat");
} else {
format = config_get_string(config, "SimpleOutput", "RecFormat2");
}
const char *quality = config_get_string(config, "SimpleOutput", "RecQuality");
if (strcmp(quality, "Lossless") == 0) {
ffmpegOutput = true;
}
}
if (record_advanced_settings) {
bool use_native = strcmp(format, "hybrid_mp4") == 0;
const char *output_id = use_native ? "mp4_output" : "ffmpeg_muxer";
if (!recordOutput || strcmp(obs_output_get_id(recordOutput), output_id) != 0) {
obs_output_release(recordOutput);
recordOutput = obs_output_create(output_id, "vertical_canvas_record", nullptr, nullptr);
}
} else {
obs_output_t *output = obs_frontend_get_recording_output();
if (!output) {
obs_output_t *replay_output = obs_frontend_get_replay_buffer_output();
if (!replay_output) {
ShowNoReplayOutputError();
return;
}
obs_output_release(replay_output);
}
if (!recordOutput || strcmp(obs_output_get_id(recordOutput), obs_output_get_id(output)) != 0) {
obs_output_release(recordOutput);
recordOutput = obs_output_create(obs_output_get_id(output), "vertical_canvas_record", nullptr, nullptr);
}
obs_data_t *settings = obs_output_get_settings(output);
obs_output_update(recordOutput, settings);
obs_data_release(settings);
obs_output_release(output);
}
SetRecordAudioEncoders(recordOutput);
if (recordPath.empty() && (!dir || !strlen(dir))) {
if (isVisible()) {
QMessageBox::warning(this, QString::fromUtf8(obs_frontend_get_locale_string("Output.BadPath.Title")),
QString::fromUtf8(obs_module_text("RecordPathError")));
}
return;
}
const bool started_video = StartVideo();
obs_output_set_video_encoder(recordOutput, GetRecordVideoEncoder());
SetRecordAudioEncoders(recordOutput);
signal_handler_t *signal = obs_output_get_signal_handler(recordOutput);
signal_handler_disconnect(signal, "start", record_output_start, this);
signal_handler_disconnect(signal, "stop", record_output_stop, this);
signal_handler_disconnect(signal, "stopping", record_output_stopping, this);
signal_handler_connect(signal, "start", record_output_start, this);
signal_handler_connect(signal, "stop", record_output_stop, this);
signal_handler_connect(signal, "stopping", record_output_stopping, this);
std::string filenameFormat;
if (record_advanced_settings) {
if (filename_formatting.empty()) {
filename_formatting = config_get_string(config, "Output", "FilenameFormatting");
filename_formatting += "-vertical";
}
if (filename_formatting.empty()) {
filename_formatting = "%CCYY-%MM-%DD %hh-%mm-%ss-vertical";
}
filenameFormat = filename_formatting;
} else {
filenameFormat = config_get_string(config, "Output", "FilenameFormatting");
filenameFormat += "-vertical";
}
if (!format || !strlen(format)) {
format = "mkv";
}
std::string ext = format;
if (ffmpegOutput) {
ext = "avi";
} else if (ext == "hybrid_mp4") {
ext = "mp4";
} else if (ext == "fragmented_mp4") {
ext = "mp4";
} else if (ext == "fragmented_mov") {
ext = "mov";
} else if (ext == "hls") {
ext = "m3u8";
} else if (ext == "mpegts") {
ext = "ts";
}
obs_data_t *ps = obs_data_create();
char path[512];
char *filename = os_generate_formatted_filename(ext.c_str(), true, filenameFormat.c_str());
if (recordPath.empty() && dir) {
recordPath = dir;
} else {
dir = recordPath.c_str();
}
snprintf(path, 512, "%s/%s", dir, filename);
bfree(filename);
ensure_directory(path);
obs_data_set_string(ps, ffmpegOutput ? "url" : "path", path);
obs_data_set_string(ps, "path", path);
obs_data_set_string(ps, "directory", dir);
obs_data_set_string(ps, "format", filenameFormat.c_str());
obs_data_set_string(ps, "extension", ext.c_str());
obs_data_set_bool(ps, "split_file", true);
obs_data_set_int(ps, "max_size_mb", max_size_mb);
obs_data_set_int(ps, "max_time_sec", max_time_sec);
obs_output_update(recordOutput, ps);
obs_data_release(ps);
SendVendorEvent("recording_starting");
const bool success = obs_output_start(recordOutput);
if (!success) {
QMetaObject::invokeMethod(this, "OnRecordStop", Q_ARG(int, OBS_OUTPUT_ERROR),
Q_ARG(QString, QString::fromUtf8(obs_output_get_last_error(recordOutput))));
if (started_video) {
DestroyVideo();
}
}
}
void CanvasDock::StopRecord()
{
recordButton->setChecked(false);
if (obs_output_active(recordOutput)) {
SendVendorEvent("recording_stopping");
obs_output_stop(recordOutput);
}
}
void CanvasDock::record_output_start(void *data, calldata_t *calldata)
{
UNUSED_PARAMETER(calldata);
auto d = static_cast<CanvasDock *>(data);
d->SendVendorEvent("recording_started");
d->CheckReplayBuffer(true);
QMetaObject::invokeMethod(d, "OnRecordStart");
}
void CanvasDock::record_output_stop(void *data, calldata_t *calldata)
{
const char *last_error = (const char *)calldata_ptr(calldata, "last_error");
QString arg_last_error = QString::fromUtf8(last_error);
const int code = (int)calldata_int(calldata, "code");
auto d = static_cast<CanvasDock *>(data);
d->SendVendorEvent("recording_stopped");
QMetaObject::invokeMethod(d, "OnRecordStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error));
}
void CanvasDock::record_output_stopping(void *data, calldata_t *calldata)
{
UNUSED_PARAMETER(calldata);
auto d = static_cast<CanvasDock *>(data);
d->CheckReplayBuffer();
}
void CanvasDock::replay_saved(void *data, calldata_t *calldata)
{
UNUSED_PARAMETER(calldata);
auto d = static_cast<CanvasDock *>(data);
d->SendVendorEvent("backtrack_saved");
QMetaObject::invokeMethod(d, "OnReplaySaved");
}
void CanvasDock::SetRecordAudioEncoders(obs_output_t *output)
{
size_t idx = 0;
if (record_advanced_settings) {
for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) {
if ((record_audio_tracks & (1ll << i)) == 0) {
continue;
}
obs_encoder_t *aet = obs_output_get_audio_encoder(replayOutput, idx);
if (!aet && recordOutput) {
aet = obs_output_get_audio_encoder(recordOutput, idx);
}
if (aet && strcmp(obs_encoder_get_id(aet), "ffmpeg_aac") != 0) {
aet = nullptr;
}
if (!aet) {
std::string name = "vertical";
name += std::to_string(idx);
aet = obs_audio_encoder_create("ffmpeg_aac", name.c_str(), nullptr, i, nullptr);
obs_encoder_set_audio(aet, obs_get_audio());
}
obs_output_set_audio_encoder(output, aet, idx);
idx++;
}
} else {
bool r = false;
obs_output_t *main_output = obs_frontend_get_replay_buffer_output();
if (!main_output) {
r = true;
main_output = obs_frontend_get_recording_output();
}
size_t mixers = obs_output_get_mixers(main_output);
if (!mixers) {
obs_output_t *record_output = obs_frontend_get_recording_output();
if (record_output) {
mixers = obs_output_get_mixers(record_output);
obs_output_release(record_output);
}
if (!mixers) {
config_t *config = obs_frontend_get_profile_config();
const char *mode = config_get_string(config, "Output", "Mode");
if (astrcmpi(mode, "Advanced") == 0) {
const char *recType = config_get_string(config, "AdvOut", "RecType");
if (astrcmpi(recType, "FFmpeg") == 0) {
mixers = config_get_int(config, "AdvOut", "FFAudioMixes");
} else {
mixers = config_get_int(config, "AdvOut", "RecTracks");
}
} else {
const char *quality = config_get_string(config, "SimpleOutput", "RecQuality");
if (strcmp(quality, "Stream") == 0) {
mixers = 1;
} else {
mixers = config_get_int(config, "SimpleOutput", "RecTracks");
}
}
if (!mixers) {
mixers = 1;
}
}
}
for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) {
if ((mixers & (1ll << i)) == 0) {
continue;
}
obs_encoder_t *aef = obs_output_get_audio_encoder(main_output, idx);
if (!aef && idx == 0) {
if (r) {
obs_frontend_recording_start();
obs_frontend_recording_stop();
} else {
obs_frontend_replay_buffer_start();
obs_frontend_replay_buffer_stop();
}
aef = obs_output_get_audio_encoder(main_output, idx);
}
if (aef) {
obs_encoder_t *aet = obs_output_get_audio_encoder(replayOutput, idx);
if (!aet && recordOutput) {
aet = obs_output_get_audio_encoder(recordOutput, idx);
}
if (aet && strcmp(obs_encoder_get_id(aef), obs_encoder_get_id(aet)) != 0) {
aet = nullptr;
}
if (!aet) {
std::string name = obs_encoder_get_name(aef);
name += "_vertical";
aet = obs_audio_encoder_create(obs_encoder_get_id(aef), name.c_str(), nullptr, i, nullptr);
obs_encoder_set_audio(aet, obs_get_audio());
}
auto s = obs_encoder_get_settings(aef);
obs_encoder_update(aet, s);
obs_data_release(s);
obs_output_set_audio_encoder(output, aet, idx);
idx++;
}
}
obs_output_release(main_output);
}
for (; idx < MAX_AUDIO_MIXES; idx++) {
obs_output_set_audio_encoder(output, nullptr, idx);
}
}
void CanvasDock::ShowNoReplayOutputError()
{
config_t *config = obs_frontend_get_profile_config();
const char *mode = config_get_string(config, "Output", "Mode");
if (astrcmpi(mode, "Advanced") == 0) {
const char *recType = config_get_string(config, "AdvOut", "RecType");
if (astrcmpi(recType, "FFmpeg") == 0) {
blog(LOG_WARNING, "[Vertical Canvas] error starting backtrack: custom ffmpeg");
if (isVisible()) {
QMessageBox::warning(this, QString::fromUtf8(obs_module_text("backtrackStartFail")),
QString::fromUtf8(obs_module_text("backtrackCustomFfmpeg")));
}
return;
}
}
blog(LOG_WARNING, "[Vertical Canvas] error starting backtrack: no replay buffer found");
if (isVisible()) {
QMessageBox::warning(this, QString::fromUtf8(obs_module_text("backtrackStartFail")),
QString::fromUtf8(obs_module_text("backtrackNoReplayBuffer")));
}
}
void CanvasDock::StartReplayBuffer()
{
if (obs_output_active(replayOutput)) {
return;
}
if (record_advanced_settings) {
if (!replayDuration) {
replayDuration = 5;
}
auto s = obs_data_create();
obs_data_set_int(s, "max_time_sec", replayDuration);
obs_data_set_int(s, "max_size_mb", 0);
if (filename_formatting.empty()) {
filename_formatting = "%CCYY-%MM-%DD %hh-%mm-%ss";
}
obs_data_set_string(s, "format", filename_formatting.c_str());
replayFilename = filename_formatting;
if (file_format.empty()) {
file_format = "mkv";
}
std::string ext = file_format;
if (ext == "hybrid_mp4") {
ext = "mp4";
} else if (ext == "fragmented_mp4") {
ext = "mp4";
} else if (ext == "fragmented_mov") {
ext = "mov";
} else if (ext == "hls") {
ext = "m3u8";
} else if (ext == "mpegts") {
ext = "ts";
}
obs_data_set_string(s, "extension", ext.c_str());
//allow_spaces
obs_data_set_string(s, "directory", replayPath.c_str());
obs_output_update(replayOutput, s);
obs_data_release(s);
} else {
obs_output_t *replay_output = obs_frontend_get_replay_buffer_output();
if (!replay_output) {
ShowNoReplayOutputError();
return;
}
obs_encoder_t *enc = obs_output_get_video_encoder(replay_output);
if (!enc) {
obs_frontend_replay_buffer_start();
obs_frontend_replay_buffer_stop();
enc = obs_output_get_video_encoder(replay_output);
}
if (!enc) {
obs_output_release(replay_output);
blog(LOG_WARNING, "[Vertical Canvas] error starting backtrack: no video encoder found");
return;
}
auto settings = obs_output_get_settings(replay_output);
obs_output_release(replay_output);
if (!strlen(obs_data_get_string(settings, "directory"))) {
obs_frontend_replay_buffer_start();
obs_frontend_replay_buffer_stop();
}
if (!replayDuration) {
replayDuration = (uint32_t)obs_data_get_int(settings, "max_time_sec");
if (!replayDuration) {
replayDuration = 5;
}
}
if (replayPath.empty()) {
replayPath = obs_data_get_string(settings, "directory");
}
obs_output_update(replayOutput, settings);
if (obs_data_get_int(settings, "max_time_sec") != replayDuration) {
const auto s = obs_output_get_settings(replayOutput);
obs_data_set_int(s, "max_time_sec", replayDuration);
obs_data_release(s);
}
if (obs_data_get_int(settings, "max_size_mb")) {
const auto s = obs_output_get_settings(replayOutput);
obs_data_set_int(s, "max_size_mb", 0);
obs_data_release(s);
}
if (strcmp(replayPath.c_str(), obs_data_get_string(settings, "directory")) != 0) {
const auto s = obs_output_get_settings(replayOutput);
obs_data_set_string(s, "directory", replayPath.c_str());
obs_data_release(s);
}
bool changed_format = false;
std::string format = obs_data_get_string(settings, "format");
size_t start_pos = format.find("Replay");
if (start_pos != std::string::npos) {
format.replace(start_pos, 6, "Backtrack");
changed_format = true;
}
start_pos = format.find("replay");
if (start_pos != std::string::npos) {
format.replace(start_pos, 6, "backtrack");
changed_format = true;
}
if (!changed_format) {
format += "-backtrack";
changed_format = true;
}
replayFilename = format;
if (changed_format) {
const auto s = obs_output_get_settings(replayOutput);
obs_data_set_string(s, "format", format.c_str());
obs_data_release(s);
}
obs_data_release(settings);
obs_output_update(replayOutput, nullptr);
}
SetRecordAudioEncoders(replayOutput);
bool started_video = StartVideo();
bool enc_set = false;
if (recordOutput) {
auto re = obs_output_get_video_encoder(recordOutput);
if (re && obs_encoder_active(re)) {
obs_output_set_video_encoder(replayOutput, re);
enc_set = true;
}
}
auto profile_config = obs_frontend_get_profile_config();
if (!enc_set && config_get_bool(profile_config, "Stream1", "EnableMultitrackVideo")) {
auto canvas_id = config_get_string(profile_config, "Stream1", "MultitrackExtraCanvas");
if (canvas_id && strcmp(canvas_id, obs_canvas_get_uuid(canvas)) == 0) {
auto streaming_output = obs_frontend_get_streaming_output();
if (streaming_output) {
for (size_t idx = 0; idx < MAX_OUTPUT_VIDEO_ENCODERS; idx++) {
auto enc = obs_output_get_video_encoder2(streaming_output, idx);
if (enc && obs_encoder_active(enc) &&
obs_encoder_video(enc) == obs_canvas_get_video(canvas)) {
obs_output_set_video_encoder(replayOutput, enc);
enc_set = true;
break;
}
}
obs_output_release(streaming_output);
}
}
}
if (!enc_set) {
obs_output_set_video_encoder(replayOutput, GetRecordVideoEncoder());
}
signal_handler_t *signal = obs_output_get_signal_handler(replayOutput);
signal_handler_disconnect(signal, "start", replay_output_start, this);
signal_handler_disconnect(signal, "stop", replay_output_stop, this);
signal_handler_connect(signal, "start", replay_output_start, this);
signal_handler_connect(signal, "stop", replay_output_stop, this);
SendVendorEvent("backtrack_starting");
const bool success = obs_output_start(replayOutput);
if (!success) {
QMetaObject::invokeMethod(this, "OnReplayBufferStop", Q_ARG(int, OBS_OUTPUT_ERROR),
Q_ARG(QString, QString::fromUtf8(obs_output_get_last_error(replayOutput))));
if (started_video) {
DestroyVideo();
}
} else {
QMetaObject::invokeMethod(this, "OnReplayBufferStart");
}
}
#define SIMPLE_ENCODER_X264 "x264"
#define SIMPLE_ENCODER_X264_LOWCPU "x264_lowcpu"
#define SIMPLE_ENCODER_QSV "qsv"
#define SIMPLE_ENCODER_QSV_AV1 "qsv_av1"
#define SIMPLE_ENCODER_NVENC "nvenc"
#define SIMPLE_ENCODER_NVENC_AV1 "nvenc_av1"
#define SIMPLE_ENCODER_NVENC_HEVC "nvenc_hevc"
#define SIMPLE_ENCODER_AMD "amd"
#define SIMPLE_ENCODER_AMD_HEVC "amd_hevc"
#define SIMPLE_ENCODER_AMD_AV1 "amd_av1"
#define SIMPLE_ENCODER_APPLE_H264 "apple_h264"
#define SIMPLE_ENCODER_APPLE_HEVC "apple_hevc"
bool EncoderAvailable(const char *encoder)
{
const char *val;
int i = 0;
while (obs_enum_encoder_types(i++, &val)) {
if (strcmp(val, encoder) == 0) {
return true;
}
}
return false;
}
const char *get_simple_output_encoder(const char *encoder)
{
if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) {
return "obs_x264";
}
if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) {
return "obs_x264";
}
if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) {
return "obs_qsv11_v2";
}
if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) {
return "obs_qsv11_av1";
}
if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) {
return "h264_texture_amf";
}
if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) {
return "h265_texture_amf";
}
if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) {
return "av1_texture_amf";
}
if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) {
return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex"
: (EncoderAvailable("jim_nvenc") ? "jim_nvenc" : "ffmpeg_nvenc");
}
if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) {
return EncoderAvailable("obs_nvenc_hevc_tex")
? "obs_nvenc_hevc_tex"
: (EncoderAvailable("jim_hevc_nvenc") ? "jim_hevc_nvenc" : "ffmpeg_hevc_nvenc");
}
if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) {
return EncoderAvailable("obs_nvenc_av1_tex") ? "obs_nvenc_av1_tex" : "jim_av1_nvenc";
}
if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) {
return "com.apple.videotoolbox.videoencoder.ave.avc";
}
if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) {
return "com.apple.videotoolbox.videoencoder.ave.hevc";
}
return "obs_x264";
}
obs_encoder_t *CanvasDock::GetStreamVideoEncoder()
{
obs_encoder_t *video_encoder = nullptr;
const char *enc_id = nullptr;
obs_data_t *video_settings = nullptr;
bool useRecordEncoder = false;
config_t *config = obs_frontend_get_profile_config();
const char *mode = config_get_string(config, "Output", "Mode");
if (stream_advanced_settings) {
video_settings = stream_encoder_settings;
obs_data_addref(video_settings);
enc_id = stream_encoder.c_str();
if (record_advanced_settings) {
useRecordEncoder = record_encoder.empty();
} else if (strcmp(mode, "Advanced") == 0) {
const char *recordEncoder = config_get_string(config, "AdvOut", "RecEncoder");
useRecordEncoder = astrcmpi(recordEncoder, "none") == 0;
} else {
const char *quality = config_get_string(config, "SimpleOutput", "RecQuality");
if (strcmp(quality, "Stream") == 0) {
useRecordEncoder = true;
}
}
} else {
if (config_get_bool(config, "Stream1", "EnableMultitrackVideo")) {
auto canvas_id = config_get_string(config, "Stream1", "MultitrackExtraCanvas");
if (canvas_id && strcmp(canvas_id, obs_canvas_get_uuid(canvas)) == 0) {
auto streaming_output = obs_frontend_get_streaming_output();
if (streaming_output) {
for (size_t idx = 0; idx < MAX_OUTPUT_VIDEO_ENCODERS; idx++) {
auto enc = obs_output_get_video_encoder2(streaming_output, idx);
if (enc && obs_encoder_active(enc) &&
obs_encoder_video(enc) == obs_canvas_get_video(canvas)) {
obs_output_release(streaming_output);
return enc;
}
}
obs_output_release(streaming_output);
}
}
}
if (strcmp(mode, "Advanced") == 0) {
video_settings = GetDataFromJsonFile("streamEncoder.json");
enc_id = config_get_string(config, "AdvOut", "Encoder");
const char *recordEncoder = config_get_string(config, "AdvOut", "RecEncoder");
useRecordEncoder = astrcmpi(recordEncoder, "none") == 0;
if (!streamingVideoBitrate) {
streamingVideoBitrate = (uint32_t)obs_data_get_int(video_settings, "bitrate");
} else {
obs_data_set_int(video_settings, "bitrate", streamingVideoBitrate);
}
} else {
video_settings = obs_data_create();
bool advanced = config_get_bool(config, "SimpleOutput", "UseAdvanced");
enc_id = get_simple_output_encoder(config_get_string(config, "SimpleOutput", "StreamEncoder"));
const char *presetType;
const char *preset;
if (strcmp(enc_id, SIMPLE_ENCODER_QSV) == 0) {
presetType = "QSVPreset";
} else if (strcmp(enc_id, SIMPLE_ENCODER_QSV_AV1) == 0) {
presetType = "QSVPreset";
} else if (strcmp(enc_id, SIMPLE_ENCODER_AMD) == 0) {
presetType = "AMDPreset";
} else if (strcmp(enc_id, SIMPLE_ENCODER_AMD_HEVC) == 0) {
presetType = "AMDPreset";
} else if (strcmp(enc_id, SIMPLE_ENCODER_NVENC) == 0) {
presetType = "NVENCPreset2";
} else if (strcmp(enc_id, SIMPLE_ENCODER_NVENC_HEVC) == 0) {
presetType = "NVENCPreset2";
} else if (strcmp(enc_id, SIMPLE_ENCODER_AMD_AV1) == 0) {
presetType = "AMDAV1Preset";
} else if (strcmp(enc_id, SIMPLE_ENCODER_NVENC_AV1) == 0) {
presetType = "NVENCPreset2";
} else {
presetType = "Preset";
}
preset = config_get_string(config, "SimpleOutput", presetType);
obs_data_set_string(video_settings, (strcmp(presetType, "NVENCPreset2") == 0) ? "preset2" : "preset",
preset);
obs_data_set_string(video_settings, "rate_control", "CBR");
if (!streamingVideoBitrate) {
const int sVideoBitrate = (int)config_get_uint(config, "SimpleOutput", "VBitrate");
obs_data_set_int(video_settings, "bitrate", sVideoBitrate);
streamingVideoBitrate = sVideoBitrate;
} else {
obs_data_set_int(video_settings, "bitrate", streamingVideoBitrate);
}
if (advanced) {
const char *custom = config_get_string(config, "SimpleOutput", "x264Settings");
obs_data_set_string(video_settings, "x264opts", custom);
}
const char *quality = config_get_string(config, "SimpleOutput", "RecQuality");
if (strcmp(quality, "Stream") == 0) {
useRecordEncoder = true;
}
}
}
obs_encoder_t *se = nullptr;
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
se = obs_output_get_video_encoder(it->output);
if (se) {
break;
}
}
if (se && strcmp(enc_id, obs_encoder_get_id(se)) == 0) {
video_encoder = se;
}
if (!video_encoder && useRecordEncoder && recordOutput) {
auto re = obs_output_get_video_encoder(recordOutput);
if (re && strcmp(enc_id, obs_encoder_get_id(re)) == 0) {
video_encoder = re;
}
}
if (!video_encoder && useRecordEncoder && replayOutput) {
auto re = obs_output_get_video_encoder(replayOutput);
if (re && strcmp(enc_id, obs_encoder_get_id(re)) == 0) {
video_encoder = re;
}
}
if (!video_encoder) {
video_encoder = obs_video_encoder_create(enc_id, "vertical_canvas_video_encoder", nullptr, nullptr);
}
obs_encoder_update(video_encoder, video_settings);
obs_data_release(video_settings);
switch (video_output_get_format(obs_canvas_get_video(canvas))) {
case VIDEO_FORMAT_I420:
case VIDEO_FORMAT_NV12:
case VIDEO_FORMAT_I010:
case VIDEO_FORMAT_P010:
break;
default:
obs_encoder_set_preferred_video_format(video_encoder, VIDEO_FORMAT_NV12);
}
if (!obs_encoder_active(video_encoder)) {
obs_encoder_set_video(video_encoder, obs_canvas_get_video(canvas));
}
return video_encoder;
}
obs_encoder_t *CanvasDock::GetRecordVideoEncoder()
{
obs_encoder_t *video_encoder = nullptr;
const char *enc_id = nullptr;
obs_data_t *settings = nullptr;
if (record_advanced_settings) {
if (record_encoder.empty()) {
return GetStreamVideoEncoder();
} else {
enc_id = record_encoder.c_str();
settings = record_encoder_settings;
obs_data_addref(settings);
}
} else {
config_t *config = obs_frontend_get_profile_config();
const char *mode = config_get_string(config, "Output", "Mode");
if (strcmp(mode, "Advanced") == 0) {
if (astrcmpi(config_get_string(config, "AdvOut", "RecEncoder"), "none") == 0) {
return GetStreamVideoEncoder();
}
enc_id = config_get_string(config, "AdvOut", "RecEncoder");
} else {
if (strcmp(config_get_string(config, "SimpleOutput", "RecQuality"), "Stream") == 0) {
return GetStreamVideoEncoder();
}
enc_id = get_simple_output_encoder(config_get_string(config, "SimpleOutput", "RecEncoder"));
}
}
if (!video_encoder && replayOutput) {
auto re = obs_output_get_video_encoder(replayOutput);
if (re && strcmp(enc_id, obs_encoder_get_id(re)) == 0) {
video_encoder = re;
}
}
if (!video_encoder && recordOutput) {
auto re = obs_output_get_video_encoder(recordOutput);
if (re && strcmp(enc_id, obs_encoder_get_id(re)) == 0) {
video_encoder = re;
}
}
if (!video_encoder) {
video_encoder = obs_video_encoder_create(enc_id, "vertical_canvas_record_video_encoder", nullptr, nullptr);
}
obs_encoder_update(video_encoder, settings);
obs_data_release(settings);
if (!record_advanced_settings) {
obs_output_t *main_output = obs_frontend_get_replay_buffer_output();
if (!main_output) {
main_output = obs_frontend_get_recording_output();
}
auto enc = obs_output_get_video_encoder(main_output);
obs_output_release(main_output);
obs_data_t *d = obs_encoder_get_settings(enc);
obs_encoder_update(video_encoder, d);
if (!recordVideoBitrate) {
recordVideoBitrate = (uint32_t)obs_data_get_int(d, "bitrate");
} else {
auto s = obs_encoder_get_settings(video_encoder);
if (recordVideoBitrate != obs_data_get_int(s, "bitrate")) {
obs_data_set_int(s, "bitrate", recordVideoBitrate);
obs_encoder_update(video_encoder, nullptr);
}
obs_data_release(s);
}
obs_data_release(d);
}
if (!video_output_stopped(obs_canvas_get_video(canvas))) {
switch (video_output_get_format(obs_canvas_get_video(canvas))) {
case VIDEO_FORMAT_I420:
case VIDEO_FORMAT_NV12:
case VIDEO_FORMAT_I010:
case VIDEO_FORMAT_P010:
break;
default:
obs_encoder_set_preferred_video_format(video_encoder, VIDEO_FORMAT_NV12);
}
if (!obs_encoder_active(video_encoder)) {
obs_encoder_set_video(video_encoder, obs_canvas_get_video(canvas));
}
}
return video_encoder;
}
void CanvasDock::StopReplayBuffer()
{
QMetaObject::invokeMethod(this, "OnReplayBufferStop", Q_ARG(int, OBS_OUTPUT_SUCCESS),
Q_ARG(QString, QString::fromUtf8("")));
if (obs_output_active(replayOutput)) {
SendVendorEvent("backtrack_stopping");
obs_output_stop(replayOutput);
}
}
void CanvasDock::replay_output_start(void *data, calldata_t *calldata)
{
UNUSED_PARAMETER(calldata);
UNUSED_PARAMETER(data);
auto d = static_cast<CanvasDock *>(data);
d->SendVendorEvent("backtrack_started");
QMetaObject::invokeMethod(d, "OnReplayBufferStart");
}
void CanvasDock::replay_output_stop(void *data, calldata_t *calldata)
{
const char *last_error = (const char *)calldata_ptr(calldata, "last_error");
QString arg_last_error = QString::fromUtf8(last_error);
const int code = (int)calldata_int(calldata, "code");
auto d = static_cast<CanvasDock *>(data);
d->SendVendorEvent("backtrack_stopped");
QMetaObject::invokeMethod(d, "OnReplayBufferStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error));
}
void CanvasDock::StreamButtonClicked()
{
int active_count = 0;
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (obs_output_active(it->output)) {
active_count++;
}
}
if (active_count > 0) {
StopStream();
return;
}
StartStream();
}
void CanvasDock::StreamButtonMultiMenu(QMenu *menu)
{
int active_count = 0;
menu->clear();
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
bool active = obs_output_active(it->output);
if (active) {
active_count++;
}
if (!it->enabled && !active) {
continue;
}
auto a = menu->addAction(QString::fromUtf8(it->name.empty() ? it->stream_server : it->name));
a->setCheckable(true);
a->setChecked(active);
auto output = it->output;
if (active) {
connect(a, &QAction::triggered, [output] { obs_output_stop(output); });
} else {
connect(a, &QAction::triggered, [this, it] { StartStreamOutput(it); });
}
}
menu->addSeparator();
if (active_count == 0) {
menu->addAction(QString::fromUtf8(obs_module_text("StartAll")), [this] { StartStream(); });
} else {
menu->addAction(QString::fromUtf8(obs_module_text("StopAll")), [this] { StopStream(); });
}
}
void CanvasDock::StartStreamOutput(std::vector<StreamServer>::iterator it)
{
CreateStreamOutput(it);
const bool started_video = StartVideo();
if (it->settings && obs_data_get_bool(it->settings, "advanced")) {
blog(LOG_INFO, "[Vertical Canvas] Start output '%s' with advanced settings", it->name.c_str());
auto venc_name = obs_data_get_string(it->settings, "video_encoder");
if (!venc_name || venc_name[0] == '\0') {
//use main encoder
obs_output_set_video_encoder(it->output, GetStreamVideoEncoder());
} else {
obs_data_t *s = nullptr;
auto ves = obs_data_get_obj(it->settings, "video_encoder_settings");
if (ves) {
s = obs_data_create();
obs_data_apply(s, ves);
obs_data_release(ves);
}
std::string video_encoder_name = "vertical_canvas_video_encoder_";
video_encoder_name += it->name;
auto venc = obs_video_encoder_create(venc_name, video_encoder_name.c_str(), s, nullptr);
obs_data_release(s);
obs_encoder_set_video(venc, obs_canvas_get_video(canvas));
auto divisor = obs_data_get_int(it->settings, "frame_rate_divisor");
bool scale = obs_data_get_bool(it->settings, "scale");
void *handle = nullptr;
if (scale || divisor > 1)
#ifdef _WIN32
handle = os_dlopen("obs");
#else
handle = dlopen(nullptr, RTLD_LAZY);
#endif
if (divisor > 1) {
auto func =
(bool (*)(obs_encoder_t *, uint32_t))os_dlsym(handle, "obs_encoder_set_frame_rate_divisor");
if (func) {
func(venc, (uint32_t)divisor);
}
//obs_encoder_set_frame_rate_divisor(venc, (uint32_t)divisor);
}
if (scale) {
auto func = (void (*)(obs_encoder_t *, uint32_t, uint32_t))os_dlsym(handle,
"obs_encoder_set_scaled_size");
if (func) {
func(venc, (uint32_t)obs_data_get_int(it->settings, "width"),
(uint32_t)obs_data_get_int(it->settings, "height"));
}
//obs_encoder_set_scaled_size(venc, (uint32_t)obs_data_get_int(it->settings, "width"), (uint32_t)obs_data_get_int(it->settings, "height"));
auto func2 = (void (*)(obs_encoder_t *, obs_scale_type))os_dlsym(handle,
"obs_encoder_set_gpu_scale_type");
if (func2) {
func2(venc, (obs_scale_type)obs_data_get_int(it->settings, "scale_type"));
}
//obs_encoder_set_gpu_scale_type(venc, (obs_scale_type)obs_data_get_int(it->settings, "scale_type"));
}
if (handle) {
os_dlclose(handle);
}
obs_output_set_video_encoder(it->output, venc);
}
auto aenc_name = obs_data_get_string(it->settings, "audio_encoder");
if (!aenc_name || aenc_name[0] == '\0') {
//use main encoder
obs_output_set_audio_encoder(it->output, GetStreamAudioEncoder(), 0);
} else {
obs_data_t *s = nullptr;
auto aes = obs_data_get_obj(it->settings, "audio_encoder_settings");
if (aes) {
s = obs_data_create();
obs_data_apply(s, aes);
obs_data_release(aes);
}
std::string audio_encoder_name = "vertical_canvas_audio_encoder_";
audio_encoder_name += it->name;
auto aenc = obs_audio_encoder_create(aenc_name, audio_encoder_name.c_str(), s,
obs_data_get_int(it->settings, "audio_track"), nullptr);
obs_data_release(s);
obs_encoder_set_audio(aenc, obs_get_audio());
obs_output_set_audio_encoder(it->output, aenc, 0);
}
} else {
blog(LOG_INFO, "[Vertical Canvas] Start output '%s'", it->name.c_str());
obs_output_set_video_encoder(it->output, GetStreamVideoEncoder());
obs_output_set_audio_encoder(it->output, GetStreamAudioEncoder(), 0);
}
it->stopping = false;
if (!obs_output_start(it->output)) {
if (started_video) {
DestroyVideo();
}
it->stopping = true;
QMetaObject::invokeMethod(this, "OnStreamStop", Q_ARG(int, OBS_OUTPUT_ERROR),
Q_ARG(QString, QString::fromUtf8(obs_output_get_last_error(it->output))),
Q_ARG(QString, QString::fromUtf8(it->stream_server)),
Q_ARG(QString, QString::fromUtf8(it->stream_key)));
}
}
void CanvasDock::CreateStreamOutput(std::vector<StreamServer>::iterator it)
{
auto s = obs_data_create();
obs_data_set_string(s, "server", it->stream_server.c_str());
obs_data_set_string(s, "key", it->stream_key.c_str());
obs_data_set_string(s, "bearer_token", it->stream_key.c_str());
//use_auth
//username
//password
obs_service_update(it->service, s);
obs_data_release(s);
const char *type = nullptr;
#ifdef _WIN32
auto handle = os_dlopen("obs");
#else
auto handle = dlopen(nullptr, RTLD_LAZY);
#endif
if (handle) {
auto type_func = (const char *(*)(obs_service_t *))os_dlsym(handle, "obs_service_get_output_type");
if (!type_func) {
type_func = (const char *(*)(obs_service_t *))os_dlsym(handle, "obs_service_get_preferred_output_type");
}
if (type_func) {
type = type_func(it->service);
}
if (!type) {
const char *url = nullptr;
auto url_func = (const char *(*)(obs_service_t *))os_dlsym(handle, "obs_service_get_url");
if (url_func) {
url = url_func(it->service);
} else {
auto info_func = (const char *(*)(obs_service_t *,
uint32_t))os_dlsym(handle, "obs_service_get_connect_info");
if (info_func) {
url = info_func(it->service, 0); // OBS_SERVICE_CONNECT_INFO_SERVER_URL
}
}
type = "rtmp_output";
if (url != nullptr && strncmp(url, "ftl", 3) == 0) {
type = "ftl_output";
} else if (url != nullptr && strncmp(url, "rtmp", 4) != 0) {
type = "ffmpeg_mpegts_muxer";
}
}
os_dlclose(handle);
} else {
type = "rtmp_output";
}
if (!it->output || strcmp(type, obs_output_get_id(it->output)) != 0) {
if (it->output) {
if (obs_output_active(it->output)) {
obs_output_stop(it->output);
}
obs_output_release(it->output);
}
std::string name = "vertical_canvas_stream";
if (!it->name.empty()) {
name += "_";
name += it->name;
}
it->output = obs_output_create(type, name.c_str(), nullptr, nullptr);
obs_output_set_service(it->output, it->service);
}
config_t *config = obs_frontend_get_profile_config();
if (config) {
OBSDataAutoRelease output_settings = obs_data_create();
obs_data_set_string(output_settings, "bind_ip", config_get_string(config, "Output", "BindIP"));
obs_data_set_string(output_settings, "ip_family", config_get_string(config, "Output", "IPFamily"));
obs_output_update(it->output, output_settings);
}
if (stream_advanced_settings) {
obs_output_set_delay(it->output, stream_delay_enabled ? stream_delay_duration : 0,
stream_delay_preserve ? OBS_OUTPUT_DELAY_PRESERVE : 0);
} else {
bool useDelay = config_get_bool(config, "Output", "DelayEnable");
int delaySec = (int)config_get_int(config, "Output", "DelaySec");
bool preserveDelay = config_get_bool(config, "Output", "DelayPreserve");
obs_output_set_delay(it->output, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0);
}
signal_handler_t *signal = obs_output_get_signal_handler(it->output);
signal_handler_disconnect(signal, "start", stream_output_start, this);
signal_handler_disconnect(signal, "stop", stream_output_stop, this);
signal_handler_connect(signal, "start", stream_output_start, this);
signal_handler_connect(signal, "stop", stream_output_stop, this);
}
obs_encoder_t *CanvasDock::GetStreamAudioEncoder()
{
const auto audio_settings = obs_data_create();
config_t *config = obs_frontend_get_profile_config();
const char *mode = config_get_string(config, "Output", "Mode");
size_t mix_idx = 0;
if (stream_advanced_settings) {
if (stream_audio_track > 0) {
mix_idx = stream_audio_track - 1;
}
if (record_advanced_settings) {
obs_data_set_int(audio_settings, "bitrate", audioBitrate);
} else if (strcmp(mode, "Advanced") == 0) {
if (!audioBitrate) {
uint64_t streamTrack = config_get_uint(config, "AdvOut", "TrackIndex");
static const char *trackNames[] = {
"Track1Bitrate", "Track2Bitrate", "Track3Bitrate",
"Track4Bitrate", "Track5Bitrate", "Track6Bitrate",
};
const int advAudioBitrate = (int)config_get_uint(config, "AdvOut", trackNames[streamTrack - 1]);
obs_data_set_int(audio_settings, "bitrate", advAudioBitrate);
audioBitrate = advAudioBitrate;
} else {
obs_data_set_int(audio_settings, "bitrate", audioBitrate);
}
} else {
if (!audioBitrate) {
const int sAudioBitrate = (int)config_get_uint(config, "SimpleOutput", "ABitrate");
obs_data_set_int(audio_settings, "bitrate", sAudioBitrate);
audioBitrate = sAudioBitrate;
} else {
obs_data_set_int(audio_settings, "bitrate", audioBitrate);
}
}
} else if (strcmp(mode, "Advanced") == 0) {
mix_idx = config_get_uint(config, "AdvOut", "TrackIndex") - 1;
if (!audioBitrate) {
static const char *trackNames[] = {
"Track1Bitrate", "Track2Bitrate", "Track3Bitrate",
"Track4Bitrate", "Track5Bitrate", "Track6Bitrate",
};
const int advAudioBitrate = (int)config_get_uint(config, "AdvOut", trackNames[mix_idx]);
obs_data_set_int(audio_settings, "bitrate", advAudioBitrate);
audioBitrate = advAudioBitrate;
} else {
obs_data_set_int(audio_settings, "bitrate", audioBitrate);
}
} else {
obs_data_set_string(audio_settings, "rate_control", "CBR");
if (!audioBitrate) {
const int sAudioBitrate = (int)config_get_uint(config, "SimpleOutput", "ABitrate");
obs_data_set_int(audio_settings, "bitrate", sAudioBitrate);
audioBitrate = sAudioBitrate;
} else {
obs_data_set_int(audio_settings, "bitrate", audioBitrate);
}
}
obs_encoder_t *audio_encoder = nullptr;
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (!audio_encoder) {
audio_encoder = obs_output_get_audio_encoder(it->output, 0);
}
}
if (!audio_encoder) {
audio_encoder =
obs_audio_encoder_create("ffmpeg_aac", "vertical_canvas_audio_encoder", audio_settings, mix_idx, nullptr);
obs_encoder_set_audio(audio_encoder, obs_get_audio());
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
obs_output_set_audio_encoder(it->output, audio_encoder, 0);
}
} else {
obs_encoder_update(audio_encoder, audio_settings);
}
obs_data_release(audio_settings);
return audio_encoder;
}
void CanvasDock::StartStream()
{
bool to_start = false;
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (obs_output_active(it->output)) {
return;
}
if (it->enabled) {
to_start = true;
}
}
if (!to_start) {
blog(LOG_WARNING, "[Vertical Canvas] No stream output to start");
QMetaObject::invokeMethod(this, "OnStreamStop", Q_ARG(int, OBS_OUTPUT_SUCCESS),
Q_ARG(QString, QString::fromUtf8("")), Q_ARG(QString, QString::fromUtf8("")),
Q_ARG(QString, QString::fromUtf8("")));
if (isVisible()) {
QMessageBox::information(this, QString::fromUtf8(obs_module_text("NoOutputServer")),
QString::fromUtf8(obs_module_text("NoOutputServerWarning")));
}
return;
}
obs_encoder_t *video_encoder = nullptr;
obs_encoder_t *audio_encoder = nullptr;
const bool started_video = StartVideo();
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (!it->enabled) {
continue;
}
auto s = obs_data_create();
obs_data_set_string(s, "server", it->stream_server.c_str());
obs_data_set_string(s, "key", it->stream_key.c_str());
obs_data_set_string(s, "bearer_token", it->stream_key.c_str());
//use_auth
//username
//password
obs_service_update(it->service, s);
obs_data_release(s);
CreateStreamOutput(it);
if (it->settings && obs_data_get_bool(it->settings, "advanced")) {
blog(LOG_INFO, "[Vertical Canvas] Start output '%s' with multistream advanced settings", it->name.c_str());
auto venc_name = obs_data_get_string(it->settings, "video_encoder");
if (!venc_name || venc_name[0] == '\0') {
//use main encoder
if (!video_encoder) {
video_encoder = GetStreamVideoEncoder();
}
obs_output_set_video_encoder(it->output, video_encoder);
} else {
obs_data_t *ves_apply = nullptr;
auto ves = obs_data_get_obj(it->settings, "video_encoder_settings");
if (ves) {
ves_apply = obs_data_create();
obs_data_apply(ves_apply, ves);
obs_data_release(ves);
}
std::string video_encoder_name = "vertical_canvas_video_encoder_";
video_encoder_name += it->name;
auto venc = obs_video_encoder_create(venc_name, video_encoder_name.c_str(), ves_apply, nullptr);
obs_data_release(ves_apply);
obs_encoder_set_video(venc, obs_canvas_get_video(canvas));
auto divisor = obs_data_get_int(it->settings, "frame_rate_divisor");
bool scale = obs_data_get_bool(it->settings, "scale");
void *handle = nullptr;
if (scale || divisor > 1)
#ifdef _WIN32
handle = os_dlopen("obs");
#else
handle = dlopen(nullptr, RTLD_LAZY);
#endif
if (divisor > 1) {
auto func = (bool (*)(obs_encoder_t *,
uint32_t))os_dlsym(handle, "obs_encoder_set_frame_rate_divisor");
if (func) {
func(venc, (uint32_t)divisor);
}
//obs_encoder_set_frame_rate_divisor(venc, (uint32_t)divisor);
}
if (scale) {
auto func = (void (*)(obs_encoder_t *, uint32_t,
uint32_t))os_dlsym(handle, "obs_encoder_set_scaled_size");
if (func) {
func(venc, (uint32_t)obs_data_get_int(it->settings, "width"),
(uint32_t)obs_data_get_int(it->settings, "height"));
}
//obs_encoder_set_scaled_size(venc, (uint32_t)obs_data_get_int(it->settings, "width"), (uint32_t)obs_data_get_int(it->settings, "height"));
auto func2 = (void (*)(obs_encoder_t *,
obs_scale_type))os_dlsym(handle, "obs_encoder_set_gpu_scale_type");
if (func2) {
func2(venc, (obs_scale_type)obs_data_get_int(it->settings, "scale_type"));
}
//obs_encoder_set_gpu_scale_type(venc, (obs_scale_type)obs_data_get_int(it->settings, "scale_type"));
}
if (handle) {
os_dlclose(handle);
}
obs_output_set_video_encoder(it->output, venc);
}
auto aenc_name = obs_data_get_string(it->settings, "audio_encoder");
if (!aenc_name || aenc_name[0] == '\0') {
//use main encoder
if (!audio_encoder) {
audio_encoder = GetStreamAudioEncoder();
}
obs_output_set_audio_encoder(it->output, audio_encoder, 0);
} else {
obs_data_t *aes_apply = nullptr;
auto aes = obs_data_get_obj(it->settings, "audio_encoder_settings");
if (aes) {
aes_apply = obs_data_create();
obs_data_apply(aes_apply, aes);
obs_data_release(aes);
}
std::string audio_encoder_name = "vertical_canvas_audio_encoder_";
audio_encoder_name += it->name;
auto aenc = obs_audio_encoder_create(aenc_name, audio_encoder_name.c_str(), aes_apply,
obs_data_get_int(it->settings, "audio_track"), nullptr);
obs_data_release(aes_apply);
obs_encoder_set_audio(aenc, obs_get_audio());
obs_output_set_audio_encoder(it->output, aenc, 0);
}
} else {
blog(LOG_INFO, "[Vertical Canvas] Start output '%s'", it->name.c_str());
if (!video_encoder) {
video_encoder = GetStreamVideoEncoder();
}
obs_output_set_video_encoder(it->output, video_encoder);
if (!audio_encoder) {
audio_encoder = GetStreamAudioEncoder();
}
obs_output_set_audio_encoder(it->output, audio_encoder, 0);
}
}
SendVendorEvent("streaming_starting");
config_t *config = obs_frontend_get_profile_config();
bool success = false;
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (!it->enabled) {
continue;
}
if (config) {
OBSDataAutoRelease output_settings = obs_data_create();
obs_data_set_string(output_settings, "bind_ip", config_get_string(config, "Output", "BindIP"));
obs_data_set_string(output_settings, "ip_family", config_get_string(config, "Output", "IPFamily"));
obs_output_update(it->output, output_settings);
}
it->stopping = false;
if (obs_output_start(it->output)) {
success = true;
} else {
it->stopping = true;
QMetaObject::invokeMethod(this, "OnStreamStop", Q_ARG(int, OBS_OUTPUT_ERROR),
Q_ARG(QString, QString::fromUtf8(obs_output_get_last_error(it->output))),
Q_ARG(QString, QString::fromUtf8(it->stream_server)),
Q_ARG(QString, QString::fromUtf8(it->stream_key)));
}
}
if (!success && started_video) {
obs_canvas_set_channel(canvas, 0, nullptr);
}
}
void CanvasDock::StopStream()
{
streamButton->setChecked(false);
bool done = false;
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (obs_output_active(it->output)) {
obs_output_stop(it->output);
done = true;
}
}
if (done) {
SendVendorEvent("streaming_stopping");
}
CheckReplayBuffer();
}
void CanvasDock::stream_output_start(void *data, calldata_t *calldata)
{
UNUSED_PARAMETER(calldata);
auto d = static_cast<CanvasDock *>(data);
d->SendVendorEvent("streaming_started");
d->CheckReplayBuffer(true);
QMetaObject::invokeMethod(d, "OnStreamStart");
}
void CanvasDock::stream_output_stop(void *data, calldata_t *calldata)
{
const char *last_error = (const char *)calldata_ptr(calldata, "last_error");
QString arg_last_error = QString::fromUtf8(last_error);
const int code = (int)calldata_int(calldata, "code");
auto d = static_cast<CanvasDock *>(data);
d->SendVendorEvent("streaming_stopped");
QString stream_server;
QString stream_key;
obs_output_t *t = (obs_output_t *)calldata_ptr(calldata, "output");
for (auto it = d->streamOutputs.begin(); it != d->streamOutputs.end(); ++it) {
if (it->output == t) {
stream_server = QString::fromUtf8(it->stream_server);
stream_key = QString::fromUtf8(it->stream_key);
it->stopping = true;
}
}
QMetaObject::invokeMethod(d, "OnStreamStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error),
Q_ARG(QString, stream_server), Q_ARG(QString, stream_key));
}
void CanvasDock::DestroyVideo()
{
if (!canvas || !obs_canvas_has_video(canvas)) {
return;
}
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (it->output && obs_output_active(it->output)) {
return;
}
}
if (obs_output_active(replayOutput) || obs_output_active(recordOutput) ||
(obs_output_active(virtualCamOutput) && multiCanvasVideo == nullptr)) {
return;
}
if (replayOutput && obs_output_get_video_encoder(replayOutput)) {
obs_encoder_set_video(obs_output_get_video_encoder(replayOutput), nullptr);
}
if (recordOutput && obs_output_get_video_encoder(recordOutput)) {
obs_encoder_set_video(obs_output_get_video_encoder(recordOutput), nullptr);
}
if (virtualCamOutput) {
obs_output_set_media(virtualCamOutput, nullptr, obs_get_audio());
}
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (it->output && obs_output_get_video_encoder(it->output)) {
obs_encoder_set_video(obs_output_get_video_encoder(it->output), nullptr);
}
}
}
obs_scene_t *CanvasDock::GetCurrentScene()
{
return scene;
}
std::vector<QString> CanvasDock::GetScenes()
{
std::vector<QString> scenes;
if (scenesDock) {
for (int i = 0; i < scenesDock->sceneList->count(); i++) {
scenes.push_back(scenesDock->sceneList->item(i)->text());
}
} else if (scenesCombo) {
for (int i = 0; i < scenesCombo->count(); i++) {
scenes.push_back(scenesCombo->itemText(i));
}
}
return scenes;
}
bool CanvasDock::StreamingActive()
{
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (obs_output_active(it->output)) {
return true;
}
}
return false;
}
bool CanvasDock::RecordingActive()
{
return obs_output_active(recordOutput);
}
bool CanvasDock::BacktrackActive()
{
return obs_output_active(replayOutput);
}
bool CanvasDock::VirtualCameraActive()
{
return obs_output_active(virtualCamOutput);
}
obs_data_t *CanvasDock::SaveSettings()
{
auto save_data = obs_data_create();
if (!currentSceneName.isEmpty()) {
auto cs = currentSceneName.toUtf8();
obs_data_set_string(save_data, "current_scene", cs.constData());
}
if (scenesDock) {
obs_data_set_bool(save_data, "grid_mode", scenesDock->IsGridMode());
}
obs_data_set_int(save_data, "width", canvas_width);
obs_data_set_int(save_data, "height", canvas_height);
obs_data_set_int(save_data, "partner_block", partnerBlockTime);
obs_data_set_bool(save_data, "preview_disabled", preview_disabled);
obs_data_set_bool(save_data, "virtual_cam_warned", virtual_cam_warned);
obs_data_set_int(save_data, "streaming_video_bitrate", streamingVideoBitrate);
obs_data_set_bool(save_data, "streaming_match_main", streamingMatchMain);
obs_data_set_int(save_data, "record_video_bitrate", recordVideoBitrate);
obs_data_set_int(save_data, "max_size_mb", max_size_mb);
obs_data_set_int(save_data, "max_time_sec", max_time_sec);
obs_data_set_bool(save_data, "recording_match_main", recordingMatchMain);
obs_data_set_int(save_data, "audio_bitrate", audioBitrate);
obs_data_set_bool(save_data, "backtrack", startReplay);
obs_data_set_int(save_data, "backtrack_seconds", replayDuration);
obs_data_set_string(save_data, "backtrack_path", replayPath.c_str());
if (replayOutput) {
auto hotkeys = obs_hotkeys_save_output(replayOutput);
obs_data_set_obj(save_data, "backtrack_hotkeys", hotkeys);
obs_data_release(hotkeys);
}
obs_data_set_int(save_data, "virtual_camera_mode", virtual_cam_mode);
obs_data_array_t *stream_servers = SaveStreamOutputs();
obs_data_set_array(save_data, "stream_outputs", stream_servers);
obs_data_array_release(stream_servers);
obs_data_set_bool(save_data, "stream_delay_enabled", stream_delay_enabled);
obs_data_set_int(save_data, "stream_delay_duration", stream_delay_duration);
obs_data_set_bool(save_data, "stream_delay_preserve", stream_delay_preserve);
obs_data_set_bool(save_data, "stream_advanced_settings", stream_advanced_settings);
obs_data_set_int(save_data, "stream_audio_track", stream_audio_track);
obs_data_set_string(save_data, "stream_encoder", stream_encoder.c_str());
obs_data_set_obj(save_data, "stream_encoder_settings", stream_encoder_settings);
obs_data_set_string(save_data, "record_path", recordPath.c_str());
obs_data_set_bool(save_data, "record_advanced_settings", record_advanced_settings);
obs_data_set_string(save_data, "filename_formatting", filename_formatting.c_str());
obs_data_set_string(save_data, "file_format", file_format.c_str());
obs_data_set_int(save_data, "record_audio_tracks", record_audio_tracks);
obs_data_set_string(save_data, "record_encoder", record_encoder.c_str());
obs_data_set_obj(save_data, "record_encoder_settings", record_encoder_settings);
obs_data_array_t *start_hotkey = nullptr;
obs_data_array_t *stop_hotkey = nullptr;
obs_hotkey_pair_save(backtrack_hotkey, &start_hotkey, &stop_hotkey);
obs_data_set_array(save_data, "start_backtrack_hotkey", start_hotkey);
obs_data_set_array(save_data, "stop_backtrack_hotkey", stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
start_hotkey = nullptr;
stop_hotkey = nullptr;
obs_hotkey_pair_save(virtual_cam_hotkey, &start_hotkey, &stop_hotkey);
obs_data_set_array(save_data, "start_virtual_cam_hotkey", start_hotkey);
obs_data_set_array(save_data, "stop_virtual_cam_hotkey", stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
start_hotkey = nullptr;
stop_hotkey = nullptr;
obs_hotkey_pair_save(record_hotkey, &start_hotkey, &stop_hotkey);
obs_data_set_array(save_data, "start_record_hotkey", start_hotkey);
obs_data_set_array(save_data, "stop_record_hotkey", stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
start_hotkey = nullptr;
stop_hotkey = nullptr;
obs_hotkey_pair_save(stream_hotkey, &start_hotkey, &stop_hotkey);
obs_data_set_array(save_data, "start_stream_hotkey", start_hotkey);
obs_data_set_array(save_data, "stop_stream_hotkey", stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
start_hotkey = nullptr;
stop_hotkey = nullptr;
obs_hotkey_pair_save(pause_hotkey, &start_hotkey, &stop_hotkey);
obs_data_set_array(save_data, "pause_hotkey", start_hotkey);
obs_data_set_array(save_data, "unpause_hotkey", stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
start_hotkey = nullptr;
stop_hotkey = nullptr;
obs_hotkey_pair_save(preview_hotkey, &start_hotkey, &stop_hotkey);
obs_data_set_array(save_data, "show_preview_hotkey", start_hotkey);
obs_data_set_array(save_data, "hide_preview_hotkey", stop_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_release(stop_hotkey);
start_hotkey = obs_hotkey_save(chapter_hotkey);
obs_data_set_array(save_data, "chapter_hotkey", start_hotkey);
obs_data_array_release(start_hotkey);
start_hotkey = obs_hotkey_save(split_hotkey);
obs_data_set_array(save_data, "split_hotkey", start_hotkey);
obs_data_array_release(start_hotkey);
obs_data_array_t *transition_array = obs_data_array_create();
for (auto transition : transitions) {
const char *id = obs_source_get_unversioned_id(transition);
if (!obs_is_source_configurable(id)) {
continue;
}
obs_data_t *transition_data = obs_save_source(transition);
if (!transition_data) {
continue;
}
obs_data_array_push_back(transition_array, transition_data);
obs_data_release(transition_data);
}
obs_data_set_array(save_data, "transitions", transition_array);
obs_data_array_release(transition_array);
obs_data_set_string(save_data, "transition", transitionsDock->transition->currentText().toUtf8().constData());
return save_data;
}
void CanvasDock::ClearScenes()
{
clearing = true;
if (scenesCombo) {
scenesCombo->clear();
}
if (scenesDock && scenesDock->sceneList->count()) {
scenesDock->sceneList->clear();
}
SwitchScene("", false);
if (canvas) {
obs_canvas_set_channel(canvas, 0, nullptr);
obs_canvas_release(canvas);
canvas = nullptr;
}
}
void CanvasDock::StopOutputs()
{
StopRecord();
StopStream();
StopReplayBuffer();
if (canvas) {
obs_canvas_set_channel(canvas, 0, nullptr);
}
}
void CanvasDock::LoadScenes()
{
clearing = false;
switching = false;
for (uint32_t i = MAX_CHANNELS - 1; i > 0; i--) {
auto s = obs_get_output_source(i);
if (s == nullptr) {
obs_set_output_source(i, transitionAudioWrapper);
break;
}
obs_source_release(s);
}
if (scenesCombo) {
scenesCombo->clear();
}
if (scenesDock) {
scenesDock->sceneList->clear();
}
StartVideo();
obs_canvas_enum_scenes(
canvas,
[](void *param, obs_source_t *src) {
auto t = (CanvasDock *)param;
struct find_hotkey {
obs_source_t *source;
bool found = false;
};
find_hotkey f = {src, false};
obs_enum_hotkeys(
[](void *param, obs_hotkey_id id, obs_hotkey_t *key) {
UNUSED_PARAMETER(id);
auto f = (find_hotkey *)param;
if (obs_hotkey_get_registerer_type(key) != OBS_HOTKEY_REGISTERER_SOURCE) {
return true;
}
auto potential_source = (obs_weak_source_t *)obs_hotkey_get_registerer(key);
if (obs_hotkey_get_registerer(key) != f->source &&
!obs_weak_source_references_source(potential_source, f->source)) {
return true;
}
if (strcmp("OBSBasic.SelectScene", obs_hotkey_get_name(key)) != 0) {
return true;
}
f->found = true;
return false;
},
&f);
if (!f.found) {
std::string ssn = obs_canvas_get_name(t->canvas);
ssn += " ";
ssn += obs_frontend_get_locale_string("Basic.Hotkeys.SelectScene");
obs_hotkey_register_source(
src, "OBSBasic.SelectScene", ssn.c_str(),
[](void *data, obs_hotkey_id, obs_hotkey_t *key, bool pressed) {
if (!pressed) {
return;
}
auto p = (CanvasDock *)data;
auto potential_source = (obs_weak_source_t *)obs_hotkey_get_registerer(key);
OBSSourceAutoRelease source = obs_weak_source_get_source(potential_source);
if (source) {
auto sn = QString::fromUtf8(obs_source_get_name(source));
QMetaObject::invokeMethod(p, "SwitchScene", Q_ARG(QString, sn),
Q_ARG(bool, true));
}
},
t);
}
auto sh = obs_source_get_signal_handler(src);
signal_handler_connect(sh, "rename", source_rename, t);
QString name = QString::fromUtf8(obs_source_get_name(src));
if (t->scenesCombo) {
t->scenesCombo->addItem(name);
}
if (t->scenesDock) {
t->scenesDock->sceneList->addItem(name);
}
obs_data_t *settings = obs_source_get_settings(src);
if ((t->currentSceneName.isEmpty() && obs_data_get_bool(settings, "canvas_active")) ||
name == t->currentSceneName) {
if (t->scenesCombo) {
t->scenesCombo->setCurrentText(name);
}
if (t->scenesDock) {
for (int j = 0; j < t->scenesDock->sceneList->count(); j++) {
auto item = t->scenesDock->sceneList->item(j);
if (item->text() != name) {
continue;
}
t->scenesDock->sceneList->setCurrentItem(item);
}
}
}
obs_data_release(settings);
return true;
},
this);
struct obs_frontend_source_list scenes = {};
obs_frontend_get_scenes(&scenes);
for (size_t i = 0; i < scenes.sources.num; i++) {
obs_source_t *src = scenes.sources.array[i];
obs_data_t *settings = obs_source_get_settings(src);
if (obs_data_get_bool(settings, "custom_size") && obs_data_get_int(settings, "cx") == canvas_width &&
obs_data_get_int(settings, "cy") == canvas_height) {
obs_data_set_bool(settings, "custom_size", false);
auto sh = obs_source_get_signal_handler(src);
signal_handler_connect(sh, "rename", source_rename, this);
std::list<obs_sceneitem_t *> group_items;
obs_scene_enum_items(
obs_scene_from_source(src),
[](obs_scene_t *, obs_sceneitem_t *item, void *param) {
if (!obs_sceneitem_is_group(item)) {
return true;
}
auto groups = (std::list<obs_sceneitem_t *> *)param;
obs_sceneitem_addref(item);
groups->push_back(item);
return true;
},
&group_items);
foreach(auto &item, group_items)
{
obs_sceneitem_remove(item);
}
obs_canvas_move_scene(obs_scene_from_source(src), canvas);
obs_source_load(src);
foreach(auto &item, group_items)
{
obs_sceneitem_release(item);
}
QString name = QString::fromUtf8(obs_source_get_name(src));
const int order = (int)obs_data_get_int(settings, "order");
if (scenesCombo) {
scenesCombo->insertItem(order, name);
}
if (scenesDock) {
scenesDock->sceneList->insertItem(order, name);
}
if ((currentSceneName.isEmpty() && obs_data_get_bool(settings, "canvas_active")) ||
name == currentSceneName) {
if (scenesCombo) {
scenesCombo->setCurrentText(name);
}
if (scenesDock) {
for (int j = 0; j < scenesDock->sceneList->count(); j++) {
auto item = scenesDock->sceneList->item(j);
if (item->text() != name) {
continue;
}
scenesDock->sceneList->setCurrentItem(item);
}
}
}
}
obs_data_release(settings);
}
obs_frontend_source_list_free(&scenes);
if (scenesDock && scenesDock->sceneList->count() > 0) {
QListWidgetItem *selectedItem = nullptr;
scenesDock->sceneList->blockSignals(true);
for (int idx = 0; idx < scenesDock->sceneList->count(); idx++) {
auto item = scenesDock->sceneList->takeItem(idx);
auto scene = obs_canvas_get_source_by_name(canvas, item->text().toUtf8().constData());
auto settings = obs_source_get_settings(scene);
const int order = (int)obs_data_get_int(settings, "order");
scenesDock->sceneList->insertItem(order, item);
if (obs_data_get_bool(settings, "canvas_active")) {
selectedItem = item;
}
obs_data_release(settings);
obs_source_release(scene);
}
scenesDock->sceneList->blockSignals(false);
if (selectedItem) {
scenesDock->sceneList->setCurrentItem(selectedItem);
}
}
if ((scenesDock && scenesDock->sceneList->count() == 0) || (scenesCombo && scenesCombo->count() == 0)) {
AddScene("", false);
}
if (scenesDock && scenesDock->sceneList->currentRow() < 0) {
scenesDock->sceneList->setCurrentRow(0);
}
}
void CanvasDock::SwitchScene(const QString &scene_name, bool transition)
{
if ((clearing || switching) && !scene_name.isEmpty()) {
return;
}
auto fs = scene_name.isEmpty() ? nullptr : obs_canvas_get_scene_by_name(canvas, scene_name.toUtf8().constData());
if (fs == scene || (fs == nullptr && !scene_name.isEmpty())) {
obs_scene_release(fs);
return;
}
auto s = obs_scene_get_source(fs);
auto oldSource = obs_scene_get_source(scene);
auto sh = oldSource ? obs_source_get_signal_handler(oldSource) : nullptr;
if (sh) {
signal_handler_disconnect(sh, "item_add", SceneItemAdded, this);
signal_handler_disconnect(sh, "reorder", SceneReordered, this);
signal_handler_disconnect(sh, "refresh", SceneRefreshed, this);
}
if (!source || obs_weak_source_references_source(source, oldSource)) {
obs_weak_source_release(source);
source = obs_source_get_weak_source(s);
if (canvas) {
obs_canvas_set_channel(canvas, 0, s);
}
} else {
oldSource = obs_weak_source_get_source(source);
if (oldSource) {
auto ost = obs_source_get_type(oldSource);
if (ost == OBS_SOURCE_TYPE_TRANSITION) {
auto private_settings = s ? obs_source_get_private_settings(s) : nullptr;
obs_source_t *override_transition =
GetTransition(obs_data_get_string(private_settings, "transition"));
if (SwapTransition(override_transition)) {
obs_source_release(oldSource);
oldSource = obs_weak_source_get_source(source);
signal_handler_t *handler = obs_source_get_signal_handler(oldSource);
signal_handler_connect(handler, "transition_stop", transition_override_stop, this);
}
int duration = 0;
if (override_transition) {
duration = (int)obs_data_get_int(private_settings, "transition_duration");
}
if (duration <= 0) {
duration = obs_frontend_get_transition_duration();
}
obs_data_release(private_settings);
auto sourceA = obs_transition_get_source(oldSource, OBS_TRANSITION_SOURCE_A);
if (sourceA != obs_scene_get_source(scene)) {
obs_transition_set(oldSource, obs_scene_get_source(scene));
}
obs_source_release(sourceA);
if (transition) {
obs_transition_start(oldSource, OBS_TRANSITION_MODE_AUTO, duration, s);
} else {
obs_transition_set(oldSource, s);
}
} else {
obs_weak_source_release(source);
source = obs_source_get_weak_source(s);
if (canvas) {
obs_canvas_set_channel(canvas, 0, s);
}
}
obs_source_release(oldSource);
} else {
obs_weak_source_release(source);
source = obs_source_get_weak_source(s);
if (canvas) {
obs_canvas_set_channel(canvas, 0, s);
}
}
}
scene = obs_scene_from_source(s);
if (scene) {
sh = obs_source_get_signal_handler(s);
if (sh) {
signal_handler_connect(sh, "item_add", SceneItemAdded, this);
signal_handler_connect(sh, "reorder", SceneReordered, this);
signal_handler_connect(sh, "refresh", SceneRefreshed, this);
}
}
auto oldName = currentSceneName;
if (!scene_name.isEmpty()) {
currentSceneName = scene_name;
}
if (scenesCombo && scenesCombo->currentText() != scene_name) {
scenesCombo->setCurrentText(scene_name);
}
if (scenesDock && !scene_name.isEmpty()) {
QListWidgetItem *item = scenesDock->sceneList->currentItem();
if (!item || item->text() != scene_name) {
for (int i = 0; i < scenesDock->sceneList->count(); i++) {
item = scenesDock->sceneList->item(i);
if (item->text() == scene_name) {
scenesDock->sceneList->setCurrentRow(i);
item->setSelected(true);
break;
}
}
}
}
if (sourcesDock) {
sourcesDock->sourceList->GetStm()->SceneChanged();
}
obs_source_release(s);
if (vendor && oldName != currentSceneName) {
const auto d = obs_data_create();
obs_data_set_int(d, "width", canvas_width);
obs_data_set_int(d, "height", canvas_height);
obs_data_set_string(d, "old_scene", oldName.toUtf8().constData());
obs_data_set_string(d, "new_scene", currentSceneName.toUtf8().constData());
obs_websocket_vendor_emit_event(vendor, "switch_scene", d);
obs_data_release(d);
}
}
void CanvasDock::transition_override_stop(void *data, calldata_t *)
{
auto dock = (CanvasDock *)data;
QMetaObject::invokeMethod(dock, "SwitchBackToSelectedTransition", Qt::QueuedConnection);
}
void CanvasDock::SwitchBackToSelectedTransition()
{
auto tn = transitionsDock->transition->currentText().toUtf8();
auto transition = GetTransition(tn.constData());
SwapTransition(transition);
}
obs_source_t *CanvasDock::GetTransition(const char *transition_name)
{
if (!transition_name || !strlen(transition_name)) {
return nullptr;
}
for (auto transition : transitions) {
if (strcmp(transition_name, obs_source_get_name(transition)) == 0) {
return transition;
}
}
return nullptr;
}
bool CanvasDock::SwapTransition(obs_source_t *newTransition)
{
if (!newTransition || obs_weak_source_references_source(source, newTransition)) {
return false;
}
obs_transition_set_size(newTransition, canvas_width, canvas_height);
obs_source_t *oldTransition = obs_weak_source_get_source(source);
if (!oldTransition || obs_source_get_type(oldTransition) != OBS_SOURCE_TYPE_TRANSITION) {
if (oldTransition) {
obs_transition_set(newTransition, oldTransition);
obs_source_release(oldTransition);
}
obs_weak_source_release(source);
source = obs_source_get_weak_source(newTransition);
if (canvas) {
obs_canvas_set_channel(canvas, 0, newTransition);
}
obs_source_inc_showing(newTransition);
obs_source_inc_active(newTransition);
return true;
}
signal_handler_t *handler = obs_source_get_signal_handler(oldTransition);
signal_handler_disconnect(handler, "transition_stop", transition_override_stop, this);
obs_source_inc_showing(newTransition);
obs_source_inc_active(newTransition);
obs_transition_swap_begin(newTransition, oldTransition);
obs_weak_source_release(source);
source = obs_source_get_weak_source(newTransition);
if (canvas) {
obs_canvas_set_channel(canvas, 0, newTransition);
}
obs_transition_swap_end(newTransition, oldTransition);
obs_source_dec_showing(oldTransition);
obs_source_dec_active(oldTransition);
obs_source_release(oldTransition);
return true;
}
void CanvasDock::source_rename(void *data, calldata_t *calldata)
{
const auto d = static_cast<CanvasDock *>(data);
const auto prev_name = QString::fromUtf8(calldata_string(calldata, "prev_name"));
const auto new_name = QString::fromUtf8(calldata_string(calldata, "new_name"));
auto source = (obs_source_t *)calldata_ptr(calldata, "source");
if (!source || !obs_source_is_scene(source)) {
return;
}
struct obs_frontend_source_list scenes = {};
obs_frontend_get_scenes(&scenes);
for (size_t i = 0; i < scenes.sources.num; i++) {
const obs_source_t *src = scenes.sources.array[i];
auto ss = obs_source_get_settings(src);
auto c = obs_data_get_array(ss, "canvas");
obs_data_release(ss);
if (!c) {
continue;
}
const auto count = obs_data_array_count(c);
for (size_t j = 0; j < count; j++) {
auto item = obs_data_array_item(c, j);
auto n = QString::fromUtf8(obs_data_get_string(item, "scene"));
if (n == prev_name) {
obs_data_set_string(item, "scene", calldata_string(calldata, "new_name"));
}
obs_data_release(item);
}
obs_data_array_release(c);
}
obs_frontend_source_list_free(&scenes);
if (d->scenesDock) {
for (int i = 0; i < d->scenesDock->sceneList->count(); i++) {
const auto item = d->scenesDock->sceneList->item(i);
if (item->text() != prev_name) {
continue;
}
item->setText(new_name);
}
}
if (d->scenesCombo) {
for (int i = 0; i < d->scenesCombo->count(); i++) {
if (d->scenesCombo->itemText(i) != prev_name) {
continue;
}
const bool selected = d->scenesCombo->currentText() == prev_name;
d->scenesCombo->removeItem(i);
d->scenesCombo->addItem(new_name);
if (selected) {
d->scenesCombo->setCurrentText(new_name);
}
}
}
}
void CanvasDock::source_remove(void *data, calldata_t *calldata)
{
const auto d = static_cast<CanvasDock *>(data);
const auto source = (obs_source_t *)calldata_ptr(calldata, "source");
if (!obs_source_is_scene(source)) {
return;
}
const auto canvas = obs_source_get_canvas(source);
obs_canvas_release(canvas);
if (!canvas || canvas != d->canvas) {
return;
}
if (obs_weak_source_references_source(d->source, source) || source == obs_scene_get_source(d->scene)) {
QMetaObject::invokeMethod(d, "SwitchScene", Q_ARG(QString, ""), Q_ARG(bool, false));
}
const auto name = QString::fromUtf8(obs_source_get_name(source));
if (name.isEmpty()) {
return;
}
QMetaObject::invokeMethod(d, "SceneRemoved", Q_ARG(QString, name));
}
void CanvasDock::SceneRemoved(const QString name)
{
if (clearing || switching) {
return;
}
if (scenesDock) {
for (int i = 0; i < scenesDock->sceneList->count(); i++) {
auto item = scenesDock->sceneList->item(i);
if (item->text() != name) {
continue;
}
scenesDock->sceneList->takeItem(i);
}
auto r = scenesDock->sceneList->currentRow();
auto c = scenesDock->sceneList->count();
if (!clearing && !switching && ((r < 0 && c > 0) || r >= c)) {
scenesDock->sceneList->setCurrentRow(0);
}
}
if (scenesCombo) {
for (int i = 0; i < scenesCombo->count(); i++) {
if (scenesCombo->itemText(i) != name) {
continue;
}
scenesCombo->removeItem(i);
}
if (!clearing && !switching && scenesCombo->currentIndex() < 0 && scenesCombo->count()) {
scenesCombo->setCurrentIndex(0);
}
}
}
void CanvasDock::source_save(void *data, calldata_t *calldata)
{
const auto d = static_cast<CanvasDock *>(data);
const auto source = (obs_source_t *)calldata_ptr(calldata, "source");
if (!obs_source_is_scene(source)) {
return;
}
obs_data_t *settings = obs_source_get_settings(source);
if (!settings) {
return;
}
const QString name = QString::fromUtf8(obs_source_get_name(source));
if (d->scenesCombo) {
obs_data_set_bool(settings, "canvas_active", d->scenesCombo->currentText() == name);
}
obs_data_release(settings);
}
void CanvasDock::FinishLoading()
{
if (!first_time) {
return;
}
if (action && !action->isChecked()) {
action->trigger();
}
auto canvasDock = (QDockWidget *)this->parentWidget();
auto main = ((QMainWindow *)canvasDock->parentWidget());
main->addDockWidget(Qt::RightDockWidgetArea, canvasDock);
canvasDock->setFloating(false);
canvasDock->show();
if (scenesDockAction && !scenesDockAction->isChecked()) {
scenesDockAction->trigger();
}
auto dock = (QDockWidget *)(scenesDock->parentWidget());
auto sd = main->findChild<QDockWidget *>(QStringLiteral("scenesDock"));
if (sd) {
auto area = main->dockWidgetArea(sd);
if (area == Qt::NoDockWidgetArea) {
main->addDockWidget(Qt::RightDockWidgetArea, dock);
main->splitDockWidget(canvasDock, dock, Qt::Horizontal);
} else {
main->addDockWidget(area, dock);
main->splitDockWidget(sd, dock, Qt::Vertical);
}
} else {
main->addDockWidget(Qt::RightDockWidgetArea, dock);
main->splitDockWidget(canvasDock, dock, Qt::Horizontal);
}
dock->setFloating(false);
dock->show();
dock = (QDockWidget *)(sourcesDock->parentWidget());
if (sourcesDockAction && !sourcesDockAction->isChecked()) {
sourcesDockAction->trigger();
}
sd = main->findChild<QDockWidget *>(QStringLiteral("sourcesDock"));
if (sd) {
auto area = main->dockWidgetArea(sd);
if (area == Qt::NoDockWidgetArea) {
main->addDockWidget(Qt::RightDockWidgetArea, dock);
main->splitDockWidget(canvasDock, dock, Qt::Horizontal);
} else {
main->addDockWidget(area, dock);
main->splitDockWidget(sd, dock, Qt::Vertical);
}
} else {
main->addDockWidget(Qt::RightDockWidgetArea, dock);
main->splitDockWidget(canvasDock, dock, Qt::Horizontal);
}
dock->setFloating(false);
dock->show();
dock = (QDockWidget *)(transitionsDock->parentWidget());
if (transitionsDockAction && !transitionsDockAction->isChecked()) {
transitionsDockAction->trigger();
}
sd = main->findChild<QDockWidget *>(QStringLiteral("transitionsDock"));
if (sd) {
auto area = main->dockWidgetArea(sd);
if (area == Qt::NoDockWidgetArea) {
main->addDockWidget(Qt::RightDockWidgetArea, dock);
main->splitDockWidget(canvasDock, dock, Qt::Horizontal);
} else {
main->addDockWidget(area, dock);
main->splitDockWidget(sd, dock, Qt::Vertical);
}
} else {
main->addDockWidget(Qt::RightDockWidgetArea, dock);
main->splitDockWidget(canvasDock, dock, Qt::Horizontal);
}
dock->setFloating(false);
dock->show();
save_canvas();
}
void CanvasDock::OnRecordStart()
{
recordButton->setChecked(true);
recordButton->setIcon(recordActiveIcon);
recordButton->setText("00:00");
recordButton->setChecked(true);
CheckReplayBuffer(true);
}
void CanvasDock::TryRemux(QString path)
{
const obs_encoder_t *videoEncoder = nullptr;
obs_output_t *ro = obs_frontend_get_recording_output();
if (ro) {
videoEncoder = obs_output_get_video_encoder(ro);
obs_output_release(ro);
}
if (!videoEncoder) {
if (!config_get_bool(obs_frontend_get_profile_config(), "Video", "AutoRemux")) {
return;
}
}
if (!videoEncoder) {
obs_frontend_replay_buffer_start();
obs_frontend_replay_buffer_stop();
ro = obs_frontend_get_recording_output();
if (ro) {
videoEncoder = obs_output_get_video_encoder(ro);
obs_output_release(ro);
}
}
if (!videoEncoder) {
obs_frontend_recording_start();
obs_frontend_recording_stop();
ro = obs_frontend_get_recording_output();
if (ro) {
videoEncoder = obs_output_get_video_encoder(ro);
obs_output_release(ro);
}
}
if (videoEncoder) {
const auto main_window = static_cast<QMainWindow *>(obs_frontend_get_main_window());
QMetaObject::invokeMethod(main_window, "RecordingFileChanged", Q_ARG(QString, path));
}
}
void CanvasDock::OnRecordStop(int code, QString last_error)
{
recordButton->setChecked(false);
recordButton->setIcon(recordInactiveIcon);
recordButton->setText("");
recordButton->setChecked(false);
HandleRecordError(code, last_error);
CheckReplayBuffer();
QTimer::singleShot(500, this, [this] { CheckReplayBuffer(); });
obs_data_t *s = obs_output_get_settings(recordOutput);
std::string path = obs_data_get_string(s, "path");
obs_data_release(s);
if (!path.empty()) {
TryRemux(QString::fromUtf8(path.c_str()));
}
}
void CanvasDock::HandleRecordError(int code, QString last_error)
{
if (code != OBS_OUTPUT_SUCCESS) {
if (!last_error.isEmpty()) {
blog(LOG_WARNING, "[Vertical Canvas] record stop error %s", last_error.toUtf8().constData());
} else {
blog(LOG_WARNING, "[Vertical Canvas] record stop error %i", code);
}
}
if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) {
QMessageBox::critical(this, QString::fromUtf8(obs_frontend_get_locale_string("Output.RecordFail.Title")),
QString::fromUtf8(obs_frontend_get_locale_string("Output.RecordFail.Unsupported")));
} else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) {
QString msg =
last_error.isEmpty()
? QString::fromUtf8(obs_frontend_get_locale_string("Output.RecordError.EncodeErrorMsg"))
: QString::fromUtf8(obs_frontend_get_locale_string("Output.RecordError.EncodeErrorMsg.LastError"))
.arg(last_error);
QMessageBox::warning(this, QString::fromUtf8(obs_frontend_get_locale_string("Output.RecordError.Title")), msg);
} else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) {
QMessageBox::warning(this, QString::fromUtf8(obs_frontend_get_locale_string("Output.RecordNoSpace.Title")),
QString::fromUtf8(obs_frontend_get_locale_string("Output.RecordNoSpace.Msg")));
} else if (code != OBS_OUTPUT_SUCCESS && isVisible()) {
QMessageBox::critical(this, QString::fromUtf8(obs_frontend_get_locale_string("Output.RecordError.Title")),
QString::fromUtf8(obs_frontend_get_locale_string("Output.RecordError.Msg")) +
(!last_error.isEmpty() ? QString::fromUtf8("\n\n") + last_error
: QString::fromUtf8("")));
}
}
void CanvasDock::OnReplaySaved()
{
std::string path;
statusLabel->setText(QString::fromUtf8(obs_module_text("Saved")));
if (replayOutput) {
proc_handler_t *ph = obs_output_get_proc_handler(replayOutput);
if (ph) {
calldata_t cd = {0};
if (proc_handler_call(ph, "get_last_replay", &cd)) {
const char *p = calldata_string(&cd, "path");
if (p) {
path = p;
}
}
calldata_free(&cd);
}
}
if (!path.empty()) {
TryRemux(QString::fromUtf8(path.c_str()));
}
replayStatusResetTimer.start(4000);
}
void CanvasDock::OnStreamStart()
{
streamButton->setChecked(true);
streamButton->setIcon(streamActiveIcon);
streamButton->setText("00:00");
streamButton->setChecked(true);
CheckReplayBuffer(true);
}
#ifndef OBS_OUTPUT_HDR_DISABLED
#define OBS_OUTPUT_HDR_DISABLED -9
#endif // ! OBS_OUTPUT_HDR_DISABLED
void CanvasDock::OnStreamStop(int code, QString last_error, QString stream_server, QString stream_key)
{
bool active = false;
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (stream_server == QString::fromUtf8(it->stream_server) && stream_key == QString::fromUtf8(it->stream_key)) {
} else if (obs_output_active(it->output)) {
active = true;
}
}
if (!active) {
streamButton->setChecked(false);
streamButton->setIcon(streamInactiveIcon);
streamButton->setText("");
streamButton->setChecked(false);
}
const char *errorDescription = "";
bool use_last_error = false;
bool encode_error = false;
switch (code) {
case OBS_OUTPUT_BAD_PATH:
errorDescription = obs_frontend_get_locale_string("Output.ConnectFail.BadPath");
break;
case OBS_OUTPUT_CONNECT_FAILED:
use_last_error = true;
if (stream_server.contains("tiktok")) {
last_error = QString::fromUtf8(obs_module_text("tiktokError"));
}
errorDescription = obs_frontend_get_locale_string("Output.ConnectFail.ConnectFailed");
break;
case OBS_OUTPUT_INVALID_STREAM:
if (stream_server.contains("tiktok")) {
use_last_error = true;
last_error = QString::fromUtf8(obs_module_text("tiktokError"));
}
errorDescription = obs_frontend_get_locale_string("Output.ConnectFail.InvalidStream");
break;
case OBS_OUTPUT_ENCODE_ERROR:
encode_error = true;
break;
case OBS_OUTPUT_HDR_DISABLED:
errorDescription = obs_frontend_get_locale_string("Output.ConnectFail.HdrDisabled");
break;
default:
case OBS_OUTPUT_ERROR:
use_last_error = true;
errorDescription = obs_frontend_get_locale_string("Output.ConnectFail.Error");
break;
case OBS_OUTPUT_DISCONNECTED:
/* doesn't happen if output is set to reconnect. note that
* reconnects are handled in the output, not in the UI */
use_last_error = true;
errorDescription = obs_frontend_get_locale_string("Output.ConnectFail.Disconnected");
}
if (code != OBS_OUTPUT_SUCCESS) {
if (use_last_error && !last_error.isEmpty()) {
blog(LOG_WARNING, "[Vertical Canvas] stream stop error %s", last_error.toUtf8().constData());
} else {
blog(LOG_WARNING, "[Vertical Canvas] stream stop error %i", code);
}
}
if (encode_error) {
QString msg = last_error.isEmpty()
? QString::fromUtf8(obs_frontend_get_locale_string("Output.StreamEncodeError.Msg"))
: QString::fromUtf8(obs_frontend_get_locale_string("Output.StreamEncodeError.Msg.LastError"))
.arg(last_error);
QMessageBox::information(this, QString::fromUtf8(obs_frontend_get_locale_string("Output.StreamEncodeError.Title")),
msg);
} else if (code != OBS_OUTPUT_SUCCESS && isVisible()) {
QMessageBox::information(this, QString::fromUtf8(obs_frontend_get_locale_string("Output.ConnectFail.Title")),
QString::fromUtf8(errorDescription) + (use_last_error && !last_error.isEmpty()
? QString::fromUtf8("\n\n") + last_error
: QString::fromUtf8("")));
}
CheckReplayBuffer();
QTimer::singleShot(500, this, [this] { CheckReplayBuffer(); });
}
void CanvasDock::OnReplayBufferStart()
{
replayButton->setIcon(replayActiveIcon);
replayButton->setChecked(true);
}
void CanvasDock::OnReplayBufferStop(int code, QString last_error)
{
replayButton->setIcon(replayInactiveIcon);
replayButton->setChecked(false);
if (!replayStatusResetTimer.isActive()) {
replayStatusResetTimer.start(4000);
}
if (restart_video) {
ProfileChanged();
}
HandleRecordError(code, last_error);
if (code == OBS_OUTPUT_SUCCESS) {
CheckReplayBuffer(true);
QTimer::singleShot(500, this, [this] { CheckReplayBuffer(true); });
}
}
void CanvasDock::MainSceneChanged()
{
auto current_scene = obs_frontend_get_current_scene();
if (!current_scene) {
if (linkedButton) {
linkedButton->setChecked(false);
}
return;
}
auto ss = obs_source_get_settings(current_scene);
obs_source_release(current_scene);
auto c = obs_data_get_array(ss, "canvas");
obs_data_release(ss);
if (!c) {
if (linkedButton) {
linkedButton->setChecked(false);
}
return;
}
const auto count = obs_data_array_count(c);
obs_data_t *found = nullptr;
for (size_t i = 0; i < count; i++) {
auto item = obs_data_array_item(c, i);
if (!item) {
continue;
}
if (obs_data_get_int(item, "width") == canvas_width && obs_data_get_int(item, "height") == canvas_height) {
found = item;
break;
}
obs_data_release(item);
}
if (found) {
auto sn = QString::fromUtf8(obs_data_get_string(found, "scene"));
SwitchScene(sn);
if (linkedButton) {
linkedButton->setChecked(true);
}
} else if (linkedButton) {
linkedButton->setChecked(false);
}
obs_data_release(found);
obs_data_array_release(c);
}
bool CanvasDock::start_virtual_cam_hotkey(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return false;
}
const auto d = static_cast<CanvasDock *>(data);
if (obs_output_active(d->virtualCamOutput)) {
return false;
}
QMetaObject::invokeMethod(d, "VirtualCamButtonClicked");
return true;
}
bool CanvasDock::stop_virtual_cam_hotkey(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return false;
}
const auto d = static_cast<CanvasDock *>(data);
if (!obs_output_active(d->virtualCamOutput)) {
return false;
}
QMetaObject::invokeMethod(d, "VirtualCamButtonClicked");
return true;
}
bool CanvasDock::start_recording_hotkey(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return false;
}
const auto d = static_cast<CanvasDock *>(data);
if (obs_output_active(d->recordOutput)) {
return false;
}
QMetaObject::invokeMethod(d, "RecordButtonClicked");
return true;
}
bool CanvasDock::stop_recording_hotkey(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return false;
}
const auto d = static_cast<CanvasDock *>(data);
if (!obs_output_active(d->recordOutput)) {
return false;
}
QMetaObject::invokeMethod(d, "RecordButtonClicked");
return true;
}
bool CanvasDock::start_streaming_hotkey(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return false;
}
const auto d = static_cast<CanvasDock *>(data);
for (auto it = d->streamOutputs.begin(); it != d->streamOutputs.end(); ++it) {
if (obs_output_active(it->output)) {
return false;
}
}
QMetaObject::invokeMethod(d, "StreamButtonClicked");
return true;
}
bool CanvasDock::stop_streaming_hotkey(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return false;
}
const auto d = static_cast<CanvasDock *>(data);
bool found = false;
for (auto it = d->streamOutputs.begin(); it != d->streamOutputs.end(); ++it) {
if (obs_output_active(it->output)) {
found = true;
}
}
if (!found) {
return false;
}
QMetaObject::invokeMethod(d, "StreamButtonClicked");
return true;
}
bool CanvasDock::pause_recording_hotkey(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return false;
}
const auto d = static_cast<CanvasDock *>(data);
if (!obs_output_active(d->recordOutput) || obs_output_paused(d->recordOutput)) {
return false;
}
obs_output_pause(d->recordOutput, true);
return true;
}
bool CanvasDock::unpause_recording_hotkey(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return false;
}
const auto d = static_cast<CanvasDock *>(data);
if (!obs_output_active(d->recordOutput) || !obs_output_paused(d->recordOutput)) {
return false;
}
obs_output_pause(d->recordOutput, false);
return true;
}
bool CanvasDock::show_preview_hotkey(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return false;
}
const auto d = static_cast<CanvasDock *>(data);
if (!d->preview_disabled) {
return false;
}
d->preview_disabled = false;
obs_display_set_enabled(d->preview->GetDisplay(), true);
d->preview->setVisible(true);
d->previewDisabledWidget->setVisible(false);
return true;
}
bool CanvasDock::hide_preview_hotkey(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return false;
}
const auto d = static_cast<CanvasDock *>(data);
if (d->preview_disabled) {
return false;
}
d->preview_disabled = true;
obs_display_set_enabled(d->preview->GetDisplay(), false);
d->preview->setVisible(false);
d->previewDisabledWidget->setVisible(true);
return true;
}
void CanvasDock::recording_chapter_hotkey(void *data, obs_hotkey_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return;
}
const auto d = static_cast<CanvasDock *>(data);
if (!obs_output_active(d->recordOutput)) {
return;
}
proc_handler_t *ph = obs_output_get_proc_handler(d->recordOutput);
calldata cd2;
calldata_init(&cd2);
proc_handler_call(ph, "add_chapter", &cd2);
calldata_free(&cd2);
}
void CanvasDock::recording_split_hotkey(void *data, obs_hotkey_id id, obs_hotkey_t *hotkey, bool pressed)
{
UNUSED_PARAMETER(id);
UNUSED_PARAMETER(hotkey);
if (!pressed) {
return;
}
const auto d = static_cast<CanvasDock *>(data);
if (!obs_output_active(d->recordOutput)) {
return;
}
proc_handler_t *ph = obs_output_get_proc_handler(d->recordOutput);
calldata cd;
calldata_init(&cd);
proc_handler_call(ph, "split_file", &cd);
calldata_free(&cd);
}
QIcon CanvasDock::GetIconFromType(enum obs_icon_type icon_type) const
{
const auto main_window = static_cast<QMainWindow *>(obs_frontend_get_main_window());
switch (icon_type) {
case OBS_ICON_TYPE_IMAGE:
return main_window->property("imageIcon").value<QIcon>();
case OBS_ICON_TYPE_COLOR:
return main_window->property("colorIcon").value<QIcon>();
case OBS_ICON_TYPE_SLIDESHOW:
return main_window->property("slideshowIcon").value<QIcon>();
case OBS_ICON_TYPE_AUDIO_INPUT:
return main_window->property("audioInputIcon").value<QIcon>();
case OBS_ICON_TYPE_AUDIO_OUTPUT:
return main_window->property("audioOutputIcon").value<QIcon>();
case OBS_ICON_TYPE_DESKTOP_CAPTURE:
return main_window->property("desktopCapIcon").value<QIcon>();
case OBS_ICON_TYPE_WINDOW_CAPTURE:
return main_window->property("windowCapIcon").value<QIcon>();
case OBS_ICON_TYPE_GAME_CAPTURE:
return main_window->property("gameCapIcon").value<QIcon>();
case OBS_ICON_TYPE_CAMERA:
return main_window->property("cameraIcon").value<QIcon>();
case OBS_ICON_TYPE_TEXT:
return main_window->property("textIcon").value<QIcon>();
case OBS_ICON_TYPE_MEDIA:
return main_window->property("mediaIcon").value<QIcon>();
case OBS_ICON_TYPE_BROWSER:
return main_window->property("browserIcon").value<QIcon>();
case OBS_ICON_TYPE_CUSTOM:
//TODO: Add ability for sources to define custom icons
return main_window->property("defaultIcon").value<QIcon>();
case OBS_ICON_TYPE_PROCESS_AUDIO_OUTPUT:
return main_window->property("audioProcessOutputIcon").value<QIcon>();
default:
return main_window->property("defaultIcon").value<QIcon>();
}
}
QIcon CanvasDock::GetSceneIcon() const
{
const auto main_window = static_cast<QMainWindow *>(obs_frontend_get_main_window());
return main_window->property("sceneIcon").value<QIcon>();
}
QIcon CanvasDock::GetGroupIcon() const
{
const auto main_window = static_cast<QMainWindow *>(obs_frontend_get_main_window());
return main_window->property("groupIcon").value<QIcon>();
}
void CanvasDock::MainStreamStart()
{
CheckReplayBuffer(true);
if (streamingMatchMain) {
StartStream();
}
}
void CanvasDock::MainStreamStop()
{
CheckReplayBuffer();
if (streamingMatchMain) {
StopStream();
}
}
void CanvasDock::MainRecordStart()
{
CheckReplayBuffer(true);
if (recordingMatchMain) {
StartRecord();
}
}
void CanvasDock::MainRecordStop()
{
CheckReplayBuffer();
if (recordingMatchMain) {
StopRecord();
}
}
void CanvasDock::MainReplayBufferStart()
{
CheckReplayBuffer(true);
}
void CanvasDock::MainReplayBufferStop()
{
CheckReplayBuffer();
}
void CanvasDock::MainVirtualCamStart()
{
CheckReplayBuffer(true);
}
void CanvasDock::MainVirtualCamStop()
{
CheckReplayBuffer();
}
void CanvasDock::SceneReordered(void *data, calldata_t *params)
{
CanvasDock *window = static_cast<CanvasDock *>(data);
obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene");
QMetaObject::invokeMethod(window, "ReorderSources", Qt::QueuedConnection, Q_ARG(OBSScene, OBSScene(scene)));
}
void CanvasDock::ReorderSources(OBSScene order_scene)
{
if (order_scene != scene || sourcesDock->sourceList->IgnoreReorder()) {
return;
}
sourcesDock->sourceList->ReorderItems();
}
void CanvasDock::SceneRefreshed(void *data, calldata_t *params)
{
CanvasDock *window = static_cast<CanvasDock *>(data);
obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene");
QMetaObject::invokeMethod(window, "RefreshSources", Qt::QueuedConnection, Q_ARG(OBSScene, OBSScene(scene)));
}
void CanvasDock::RefreshSources(OBSScene refresh_scene)
{
if (refresh_scene != scene || sourcesDock->sourceList->IgnoreReorder()) {
return;
}
sourcesDock->sourceList->RefreshItems();
}
void CanvasDock::SceneItemAdded(void *data, calldata_t *params)
{
CanvasDock *window = static_cast<CanvasDock *>(data);
obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item");
QMetaObject::invokeMethod(window, "AddSceneItem", Qt::QueuedConnection, Q_ARG(OBSSceneItem, OBSSceneItem(item)));
}
void CanvasDock::AddSceneItem(OBSSceneItem item)
{
obs_scene_t *add_scene = obs_sceneitem_get_scene(item);
if (sourcesDock && scene == add_scene) {
sourcesDock->sourceList->Add(item);
}
obs_scene_enum_items(add_scene, select_one, (obs_sceneitem_t *)item);
}
void CanvasDock::SendVendorEvent(const char *event_name)
{
if (!vendor) {
return;
}
const auto d = obs_data_create();
obs_data_set_int(d, "width", canvas_width);
obs_data_set_int(d, "height", canvas_height);
obs_websocket_vendor_emit_event(vendor, event_name, d);
obs_data_release(d);
}
static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param)
{
if (obs_sceneitem_locked(item)) {
return true;
}
struct vec2 &offset = *reinterpret_cast<struct vec2 *>(param);
struct vec2 pos;
if (!obs_sceneitem_selected(item)) {
if (obs_sceneitem_is_group(item)) {
struct vec3 offset3;
vec3_set(&offset3, offset.x, offset.y, 0.0f);
struct matrix4 matrix;
obs_sceneitem_get_draw_transform(item, &matrix);
vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f);
matrix4_inv(&matrix, &matrix);
vec3_transform(&offset3, &offset3, &matrix);
struct vec2 new_offset;
vec2_set(&new_offset, offset3.x, offset3.y);
obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset);
}
return true;
}
obs_sceneitem_get_pos(item, &pos);
vec2_add(&pos, &pos, &offset);
obs_sceneitem_set_pos(item, &pos);
return true;
}
void CanvasDock::Nudge(int dist, MoveDir dir)
{
if (locked) {
return;
}
struct vec2 offset;
vec2_set(&offset, 0.0f, 0.0f);
switch (dir) {
case MoveDir::Up:
offset.y = (float)-dist;
break;
case MoveDir::Down:
offset.y = (float)dist;
break;
case MoveDir::Left:
offset.x = (float)-dist;
break;
case MoveDir::Right:
offset.x = (float)dist;
break;
}
obs_scene_enum_items(scene, nudge_callback, &offset);
}
void RemoveWidget(QWidget *widget);
void RemoveLayoutItem(QLayoutItem *item)
{
if (!item) {
return;
}
RemoveWidget(item->widget());
if (item->layout()) {
while (QLayoutItem *item2 = item->layout()->takeAt(0)) {
RemoveLayoutItem(item2);
}
}
delete item;
}
void RemoveWidget(QWidget *widget)
{
if (!widget) {
return;
}
if (widget->layout()) {
auto l = widget->layout();
QLayoutItem *item;
while (l->count() > 0 && (item = l->takeAt(0))) {
RemoveLayoutItem(item);
}
delete l;
}
delete widget;
}
void CanvasDock::ApiInfo(QString info)
{
auto d = obs_data_create_from_json(info.toUtf8().constData());
if (!d) {
return;
}
auto data_obj = obs_data_get_obj(d, "data");
obs_data_release(d);
if (!data_obj) {
return;
}
auto version = obs_data_get_string(data_obj, "version");
int major;
int minor;
int patch;
if (sscanf(version, "%d.%d.%d", &major, &minor, &patch) == 3) {
auto sv = MAKE_SEMANTIC_VERSION(major, minor, patch);
if (sv > MAKE_SEMANTIC_VERSION(PROJECT_VERSION_MAJOR, PROJECT_VERSION_MINOR, PROJECT_VERSION_PATCH)) {
newer_version_available = QString::fromUtf8(version);
configButton->setStyleSheet(QString::fromUtf8("background: rgb(192,128,0);"));
}
}
time_t current_time = time(nullptr);
if (current_time < partnerBlockTime || current_time - partnerBlockTime > 1209600) {
obs_data_array_t *blocks = obs_data_get_array(data_obj, "partnerBlocks");
size_t count = obs_data_array_count(blocks);
size_t added_count = 0;
for (size_t i = count; i > 0; i--) {
obs_data_t *block = obs_data_array_item(blocks, i - 1);
auto block_type = obs_data_get_string(block, "type");
QBoxLayout *layout = nullptr;
if (strcmp(block_type, "LINK") == 0) {
auto button = new QPushButton(QString::fromUtf8(obs_data_get_string(block, "label")));
button->setStyleSheet(QString::fromUtf8(obs_data_get_string(block, "qss")));
auto url = QString::fromUtf8(obs_data_get_string(block, "data"));
connect(button, &QPushButton::clicked, [url] { QDesktopServices::openUrl(QUrl(url)); });
auto buttonRow = new QHBoxLayout;
buttonRow->addWidget(button);
layout = buttonRow;
} else if (strcmp(block_type, "IMAGE") == 0) {
auto image_data = QString::fromUtf8(obs_data_get_string(block, "data"));
if (image_data.startsWith("data:image/")) {
auto pos = image_data.indexOf(";");
auto format = image_data.mid(11, pos - 11);
QImage image;
if (image.loadFromData(QByteArray::fromBase64(image_data.mid(pos + 7).toUtf8().constData()),
format.toUtf8().constData())) {
auto label = new AspectRatioPixmapLabel;
label->setPixmap(QPixmap::fromImage(image));
label->setAlignment(Qt::AlignCenter);
label->setStyleSheet(QString::fromUtf8(obs_data_get_string(block, "qss")));
auto labelRow = new QHBoxLayout;
labelRow->addWidget(label, 1, Qt::AlignCenter);
layout = labelRow;
}
}
}
if (layout) {
added_count++;
if (i == 1) {
auto closeButton = new QPushButton("🞫");
connect(closeButton, &QPushButton::clicked, [this, added_count] {
for (size_t j = 0; j < added_count; j++) {
auto item = mainLayout->takeAt(2);
RemoveLayoutItem(item);
}
partnerBlockTime = time(nullptr);
SaveSettings();
});
layout->addWidget(closeButton);
}
mainLayout->insertLayout(2, layout, 0);
}
obs_data_release(block);
}
obs_data_array_release(blocks);
}
obs_data_release(data_obj);
}
void CanvasDock::ProfileChanged()
{
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
if (obs_output_active(it->output)) {
return;
}
}
if (obs_output_active(recordOutput)) {
return;
}
if (obs_output_active(replayOutput)) {
obs_output_stop(replayOutput);
restart_video = true;
return;
}
bool virtual_cam_active = obs_output_active(virtualCamOutput);
if (virtual_cam_active) {
StopVirtualCam();
}
DestroyVideo();
StartVideo();
if (virtual_cam_active) {
StartVirtualCam();
}
if (restart_video) {
CheckReplayBuffer(true);
}
restart_video = false;
}
void CanvasDock::DeleteProjector(OBSProjector *projector)
{
for (size_t i = 0; i < projectors.size(); i++) {
if (projectors[i] == projector) {
projectors[i]->deleteLater();
projectors.erase(projectors.begin() + i);
break;
}
}
}
OBSProjector *CanvasDock::OpenProjector(int monitor)
{
/* seriously? 10 monitors? */
if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) {
return nullptr;
}
auto config = get_user_config();
if (!config) {
return nullptr;
}
bool closeProjectors = config_get_bool(config, "BasicWindow", "CloseExistingProjectors");
if (closeProjectors && monitor > -1) {
for (size_t i = projectors.size(); i > 0; i--) {
size_t idx = i - 1;
if (projectors[idx]->GetMonitor() == monitor) {
DeleteProjector(projectors[idx]);
}
}
}
OBSProjector *projector = new OBSProjector(this, monitor);
projectors.emplace_back(projector);
return projector;
}
QString GetMonitorName(const QString &id);
void CanvasDock::AddProjectorMenuMonitors(QMenu *parent, QObject *target, const char *slot)
{
QList<QScreen *> screens = QGuiApplication::screens();
for (int i = 0; i < screens.size(); i++) {
QScreen *screen = screens[i];
QRect screenGeometry = screen->geometry();
qreal ratio = screen->devicePixelRatio();
QString name = "";
#if defined(_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
QTextStream fullname(&name);
fullname << GetMonitorName(screen->name());
fullname << " (";
fullname << (i + 1);
fullname << ")";
#elif defined(__APPLE__) || defined(_WIN32)
name = screen->name();
#else
name = screen->model().simplified();
if (name.length() > 1 && name.endsWith("-")) {
name.chop(1);
}
#endif
name = name.simplified();
if (name.length() == 0) {
name = QString("%1 %2")
.arg(QString::fromUtf8(obs_frontend_get_locale_string("Display")))
.arg(QString::number(i + 1));
}
QString str = QString("%1: %2x%3 @ %4,%5")
.arg(name, QString::number(screenGeometry.width() * ratio),
QString::number(screenGeometry.height() * ratio), QString::number(screenGeometry.x()),
QString::number(screenGeometry.y()));
QAction *a = parent->addAction(str, target, slot);
a->setProperty("monitor", i);
}
}
void CanvasDock::OpenPreviewProjector()
{
int monitor = sender()->property("monitor").toInt();
OpenProjector(monitor);
}
void CanvasDock::OpenSourceProjector()
{
int monitor = sender()->property("monitor").toInt();
OBSSceneItem item = GetSelectedItem();
if (!item) {
return;
}
obs_source_t *open_source = obs_sceneitem_get_source(item);
if (!open_source) {
return;
}
obs_frontend_open_projector("Source", monitor, nullptr, obs_source_get_name(open_source));
}
void CanvasDock::updateStreamKey(const QString &newStreamKey, int index)
{
if ((int)streamOutputs.size() < index) {
return;
}
streamOutputs[index].stream_key = newStreamKey.toStdString();
}
void CanvasDock::updateStreamServer(const QString &newStreamServer, int index)
{
if ((int)streamOutputs.size() < index) {
return;
}
streamOutputs[index].stream_server = newStreamServer.toStdString();
}
QMenu *CanvasDock::CreateVisibilityTransitionMenu(bool visible, obs_sceneitem_t *si)
{
QMenu *menu = new QMenu(QString::fromUtf8(obs_frontend_get_locale_string(visible ? "ShowTransition" : "HideTransition")));
obs_source_t *curTransition = obs_sceneitem_get_transition(si, visible);
const char *curId = curTransition ? obs_source_get_id(curTransition) : nullptr;
int curDuration = (int)obs_sceneitem_get_transition_duration(si, visible);
if (curDuration <= 0) {
curDuration = obs_frontend_get_transition_duration();
}
QSpinBox *duration = new QSpinBox(menu);
duration->setMinimum(50);
duration->setSuffix(" ms");
duration->setMaximum(20000);
duration->setSingleStep(50);
duration->setValue(curDuration);
auto setTransition = [](QAction *a, bool vis, obs_sceneitem_t *si2) {
std::string id = a->property("transition_id").toString().toUtf8().constData();
if (id.empty()) {
obs_sceneitem_set_transition(si2, vis, nullptr);
} else {
obs_source_t *tr = obs_sceneitem_get_transition(si2, vis);
if (!tr || strcmp(id.c_str(), obs_source_get_id(tr)) != 0) {
QString name = QString::fromUtf8(obs_source_get_name(obs_sceneitem_get_source(si2)));
name += " ";
name += QString::fromUtf8(
obs_frontend_get_locale_string(vis ? "ShowTransition" : "HideTransition"));
tr = obs_source_create_private(id.c_str(), name.toUtf8().constData(), nullptr);
obs_sceneitem_set_transition(si2, vis, tr);
obs_source_release(tr);
int dur = (int)obs_sceneitem_get_transition_duration(si2, vis);
if (dur <= 0) {
dur = obs_frontend_get_transition_duration();
obs_sceneitem_set_transition_duration(si2, vis, dur);
}
}
if (obs_source_configurable(tr)) {
obs_frontend_open_source_properties(tr);
}
}
};
auto setDuration = [visible, si](int dur) {
obs_sceneitem_set_transition_duration(si, visible, dur);
};
connect(duration, (void (QSpinBox::*)(int))&QSpinBox::valueChanged, setDuration);
QAction *a = menu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("None")));
a->setProperty("transition_id", QString::fromUtf8(""));
a->setCheckable(true);
a->setChecked(!curId);
connect(a, &QAction::triggered, std::bind(setTransition, a, visible, si));
size_t idx = 0;
const char *id;
while (obs_enum_transition_types(idx++, &id)) {
const char *name = obs_source_get_display_name(id);
const bool match = id && curId && strcmp(id, curId) == 0;
a = menu->addAction(QString::fromUtf8(name));
a->setProperty("transition_id", QString::fromUtf8(id));
a->setCheckable(true);
a->setChecked(match);
connect(a, &QAction::triggered, std::bind(setTransition, a, visible, si));
}
QWidgetAction *durationAction = new QWidgetAction(menu);
durationAction->setDefaultWidget(duration);
menu->addSeparator();
menu->addAction(durationAction);
if (curId && obs_is_source_configurable(curId)) {
menu->addSeparator();
menu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Properties")), this,
[curTransition] { obs_frontend_open_source_properties(curTransition); });
}
return menu;
}
void CanvasDock::get_transitions(void *data, struct obs_frontend_source_list *sources)
{
auto dock = (CanvasDock *)data;
for (auto transition : dock->transitions) {
obs_source_t *tr = transition;
if (obs_source_get_ref(tr) != nullptr) {
da_push_back(sources->sources, &tr);
}
}
}
bool CanvasDock::LoadStreamOutputs(obs_data_array_t *outputs)
{
auto count = obs_data_array_count(outputs);
auto enabled_count = 0;
for (auto it = streamOutputs.begin(); it != streamOutputs.end();) {
bool found = false;
for (size_t i = 0; !found && i < count; i++) {
auto item = obs_data_array_item(outputs, i);
if (it->name == obs_data_get_string(item, "name")) {
it->stream_server = obs_data_get_string(item, "stream_server");
it->stream_key = obs_data_get_string(item, "stream_key");
it->enabled = obs_data_get_bool(item, "enabled");
if (it->enabled) {
enabled_count++;
}
obs_data_release(it->settings);
it->settings = item;
found = true;
break;
}
obs_data_release(item);
}
if (!found) {
if (obs_output_active(it->output)) {
obs_output_stop(it->output);
}
obs_service_release(obs_output_get_service(it->output));
obs_output_release(it->output);
obs_data_release(it->settings);
it = streamOutputs.erase(it);
} else {
it++;
}
}
for (size_t i = 0; i < count; i++) {
auto item = obs_data_array_item(outputs, i);
auto name = obs_data_get_string(item, "name");
bool found = false;
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); it++) {
if (it->name == name) {
found = true;
break;
}
}
if (found) {
obs_data_release(item);
continue;
}
StreamServer ss;
ss.name = name;
ss.stream_server = obs_data_get_string(item, "stream_server");
ss.stream_key = obs_data_get_string(item, "stream_key");
ss.enabled = obs_data_get_bool(item, "enabled");
if (ss.enabled) {
enabled_count++;
}
std::string service_name = "vertical_canvas_stream_service_";
service_name += std::to_string(i);
bool whip = strstr(ss.stream_server.c_str(), "whip") != nullptr;
ss.service = obs_service_create(whip ? "whip_custom" : "rtmp_custom", service_name.c_str(), nullptr, nullptr);
ss.settings = item;
streamOutputs.push_back(ss);
}
return enabled_count > 1;
}
obs_data_array_t *CanvasDock::SaveStreamOutputs()
{
obs_data_array_t *outputs = obs_data_array_create();
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); ++it) {
obs_data_t *s = it->settings;
if (s) {
obs_data_addref(s);
} else {
s = obs_data_create();
}
obs_data_set_string(s, "name", it->name.c_str());
obs_data_set_string(s, "stream_server", it->stream_server.c_str());
obs_data_set_string(s, "stream_key", it->stream_key.c_str());
obs_data_set_bool(s, "enabled", it->enabled);
obs_data_array_push_back(outputs, s);
obs_data_release(s);
}
return outputs;
}
void CanvasDock::StartStreamOutput(std::string name)
{
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); it++) {
if (it->name == name) {
StartStreamOutput(it);
break;
}
}
}
void CanvasDock::StopStreamOutput(std::string name)
{
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); it++) {
if (it->name == name) {
if (it->output) {
obs_output_stop(it->output);
}
break;
}
}
}
obs_output_t *CanvasDock::GetStreamOutput(std::string name)
{
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); it++) {
if (it->name == name) {
return obs_output_get_ref(it->output);
}
}
return nullptr;
}
void CanvasDock::UpdateMulti()
{
int enabled_count = 0;
int active_count = 0;
for (auto it = streamOutputs.begin(); it != streamOutputs.end(); it++) {
if (obs_output_active(it->output)) {
active_count++;
}
if (it->enabled) {
enabled_count++;
}
}
if (enabled_count > 1 && !multi_rtmp) {
streamButtonMulti->setVisible(true);
multi_rtmp = true;
streamButton->setChecked(active_count > 0);
streamButton->setStyleSheet(QString::fromUtf8(
"QPushButton:checked{background: rgb(0,210,153);} QPushButton{border-top-right-radius: 0; border-bottom-right-radius: 0;}"));
} else if (enabled_count <= 1 && multi_rtmp) {
streamButtonMulti->setVisible(false);
multi_rtmp = false;
streamButton->setChecked(active_count > 0);
streamButton->setStyleSheet(QString::fromUtf8("QPushButton:checked{background: rgb(0,210,153);}"));
}
}
void CanvasDock::DisableStreamSettings()
{
disable_stream_settings = true;
}
void CanvasDock::save_load(obs_data_t *save_data, bool saving, void *param)
{
UNUSED_PARAMETER(save_data);
CanvasDock *window = static_cast<CanvasDock *>(param);
if (saving) {
if (window->scenesDock) {
auto c = window->scenesDock->sceneList->count();
for (int row = 0; row < c; row++) {
auto scene_name = window->scenesDock->sceneList->item(row)->text();
auto scene = obs_canvas_get_source_by_name(window->canvas, scene_name.toUtf8().constData());
if (scene) {
auto settings = obs_source_get_settings(scene);
obs_data_set_int(settings, "order", row);
obs_data_set_bool(settings, "canvas_active", scene_name == window->currentSceneName);
obs_data_release(settings);
obs_source_release(scene);
}
}
}
if (window->scenesCombo) {
auto c = window->scenesCombo->count();
for (int row = 0; row < c; row++) {
auto scene_name = window->scenesCombo->itemText(row);
auto scene = obs_canvas_get_source_by_name(window->canvas, scene_name.toUtf8().constData());
if (scene) {
auto settings = obs_source_get_settings(scene);
obs_data_set_int(settings, "order", row);
obs_data_set_bool(settings, "canvas_active", scene_name == window->currentSceneName);
obs_data_release(settings);
obs_source_release(scene);
}
}
}
}
}
void CanvasDock::LogScenes()
{
blog(LOG_INFO, "------------------------------------------------");
blog(LOG_INFO, "[Vulcast Vertical] Canvas '%s' scenes:", obs_canvas_get_name(canvas));
if (scenesDock && scenesDock->sceneList) {
for (int j = 0; j < scenesDock->sceneList->count(); j++) {
auto item = scenesDock->sceneList->item(j);
blog(LOG_INFO, "- scene '%s':", item->text().toUtf8().constData());
auto scene = obs_canvas_get_scene_by_name(canvas, item->text().toUtf8().constData());
obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1);
obs_source_enum_filters(obs_scene_get_source(scene), LogFilter, (void *)(intptr_t)1);
obs_scene_release(scene);
}
} else if (scenesCombo) {
for (int j = 0; j < scenesCombo->count(); j++) {
auto scene_name = scenesCombo->itemText(j);
blog(LOG_INFO, "- scene '%s':", scene_name.toUtf8().constData());
auto scene = obs_canvas_get_scene_by_name(canvas, scene_name.toUtf8().constData());
obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1);
obs_source_enum_filters(obs_scene_get_source(scene), LogFilter, (void *)(intptr_t)1);
obs_scene_release(scene);
}
}
blog(LOG_INFO, "------------------------------------------------");
}
bool CanvasDock::LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val)
{
obs_source_t *source = obs_sceneitem_get_source(item);
const char *name = obs_source_get_name(source);
const char *id = obs_source_get_id(source);
int indent_count = (int)(intptr_t)v_val;
std::string indent;
for (int i = 0; i < indent_count; i++) {
indent += " ";
}
blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id);
if (obs_source_get_output_flags(source) & OBS_SOURCE_AUDIO) {
const uint32_t all_mixers = (1 << MAX_AUDIO_MIXES) - 1;
uint32_t mixers = obs_source_get_audio_mixers(source);
if (mixers == 0) {
blog(LOG_INFO, " %s- audio tracks: none", indent.c_str());
} else if ((mixers & all_mixers) != all_mixers) {
std::string tracks;
for (uint32_t i = 0; i < MAX_AUDIO_MIXES; i++) {
if (mixers & (1 << i)) {
tracks += " ";
tracks += std::to_string(i + 1);
}
}
blog(LOG_INFO, " %s- audio tracks:%s", indent.c_str(), tracks.c_str());
}
obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source);
if (monitoring_type != OBS_MONITORING_TYPE_NONE) {
const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only"
: "monitor and output";
blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type);
}
}
int child_indent = 1 + indent_count;
obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent);
obs_source_t *show_tn = obs_sceneitem_get_transition(item, true);
obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false);
if (show_tn) {
blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), obs_source_get_id(show_tn));
}
if (hide_tn) {
blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), obs_source_get_id(hide_tn));
}
if (obs_sceneitem_is_group(item)) {
obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent);
}
return true;
}
void CanvasDock::LogFilter(obs_source_t *, obs_source_t *filter, void *v_val)
{
const char *name = obs_source_get_name(filter);
const char *id = obs_source_get_id(filter);
int val = (int)(intptr_t)v_val;
std::string indent;
for (int i = 0; i < val; i++) {
indent += " ";
}
blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id);
}
void CanvasDock::AskUpdate()
{
if (newer_version_available.isEmpty()) {
return;
}
auto parts = newer_version_available.split(".");
if (parts.count() < 3) {
return;
}
int major = parts.value(0).toInt();
int minor = parts.value(1).toInt();
int patch = parts.value(2).toInt();
auto sv = MAKE_SEMANTIC_VERSION(major, minor, patch);
char *path = obs_module_config_path("config.json");
if (!path) {
return;
}
obs_data_t *config = obs_data_create_from_json_file_safe(path, "bak");
auto skip_version = config ? obs_data_get_int(config, "skip_version") : 0;
if (sv == skip_version) {
obs_data_release(config);
bfree(path);
return;
}
auto main_window = static_cast<QMainWindow *>(obs_frontend_get_main_window());
QMessageBox mb(QMessageBox::Question, QString::fromUtf8(obs_frontend_get_locale_string("Updater.Title")),
QString::fromUtf8(obs_frontend_get_locale_string("Updater.Text")) + " " +
QString::fromUtf8(obs_module_text("VerticalCanvas")) + " " + newer_version_available,
QMessageBox::StandardButtons(), main_window);
auto update = mb.addButton(QString::fromUtf8(obs_frontend_get_locale_string("Updater.UpdateNow")), QMessageBox::YesRole);
auto remind =
mb.addButton(QString::fromUtf8(obs_frontend_get_locale_string("Updater.RemindMeLater")), QMessageBox::RejectRole);
auto skip = mb.addButton(QString::fromUtf8(obs_frontend_get_locale_string("Updater.Skip")), QMessageBox::NoRole);
mb.setDefaultButton(remind);
mb.exec();
if (mb.clickedButton() == update) {
/* Stream suite prompt removed in Vulcast fork */
} else if (mb.clickedButton() == skip) {
if (!config) {
config = obs_data_create();
}
obs_data_set_int(config, "skip_version", sv);
if (obs_data_save_json_safe(config, path, "tmp", "bak")) {
blog(LOG_INFO, "[Vertical Canvas] Saved settings");
} else {
blog(LOG_ERROR, "[Vertical Canvas] Failed saving settings");
}
}
obs_data_release(config);
bfree(path);
}
LockedCheckBox::LockedCheckBox()
{
setProperty("lockCheckBox", true);
setProperty("class", "indicator-lock");
}
LockedCheckBox::LockedCheckBox(QWidget *parent) : QCheckBox(parent) {}
VisibilityCheckBox::VisibilityCheckBox()
{
setProperty("visibilityCheckBox", true);
setProperty("class", "indicator-visibility");
}
VisibilityCheckBox::VisibilityCheckBox(QWidget *parent) : QCheckBox(parent) {}
AspectRatioPixmapLabel::AspectRatioPixmapLabel(QWidget *parent) : QLabel(parent)
{
setMinimumSize(1, 1);
setScaledContents(false);
}
void AspectRatioPixmapLabel::setPixmap(const QPixmap &p)
{
pix = p;
QLabel::setPixmap(scaledPixmap());
}
int AspectRatioPixmapLabel::heightForWidth(int width) const
{
return pix.isNull() ? height() : (pix.height() * width) / pix.width();
}
QSize AspectRatioPixmapLabel::sizeHint() const
{
int w = width();
return QSize(w, heightForWidth(w));
}
QPixmap AspectRatioPixmapLabel::scaledPixmap() const
{
return pix.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
void AspectRatioPixmapLabel::resizeEvent(QResizeEvent *e)
{
UNUSED_PARAMETER(e);
if (!pix.isNull()) {
QLabel::setPixmap(scaledPixmap());
}
}