#include "vertical-canvas.hpp" #include #include "version.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 #endif OBS_DECLARE_MODULE() OBS_MODULE_AUTHOR("Aitum"); 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 "Aitum Vertical" inline std::list 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(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) { if (obs_get_module("aitum-stream-suite")) { blog(LOG_ERROR, "[Vertical Canvas] Aitum Stream Suite detected, please uninstall it to use Vertical Canvas."); return false; } 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 aitum_vertical_get_video(in int width, in int height, out ptr video)", get_video, nullptr); proc_handler_add(ph, "void aitum_vertical_get_stream_settings(in int width, in int height, out ptr outputs)", get_stream_settings, nullptr); proc_handler_add(ph, "void aitum_vertical_set_stream_settings(in int width, in int height, in ptr outputs)", set_stream_settings, nullptr); proc_handler_add(ph, "void aitum_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 aitum_vertical_start_stream_output(in int width, in int height, in string name)", start_stream_output, nullptr); proc_handler_add(ph, "void aitum_vertical_stop_stream_output(in int width, in int height, in string name)", stop_stream_output, nullptr); proc_handler_add(ph, "void aitum_vertical_add_chapter(in int width, in int height, in string chapter_name)", add_chapter, nullptr); proc_handler_add(ph, "void aitum_vertical_get_scene(in int width, in int height, out string scene)", get_scene, nullptr); proc_handler_add(ph, "void aitum_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(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("aitum-vertical-canvas"); } 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); std::string url = "https://api.aitum.tv/plugin/vertical"; const char *pguid = config_get_string(obs_frontend_get_app_config(), "General", "InstallGUID"); if (pguid) { url += "?uuid="; url += pguid; } version_update_info = update_info_create_single("[Vertical Canvas]", "OBS", url.c_str(), version_info_downloaded, nullptr); } 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); auto aitumButtonGroupLayout = new QHBoxLayout(); aitumButtonGroupLayout->setContentsMargins(0, 0, 0, 0); aitumButtonGroupLayout->setSpacing(0); auto contributeButton = new QPushButton; contributeButton->setMinimumHeight(30); QPixmap pixmap(32, 32); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); QFont font = painter.font(); font.setPixelSize(32); painter.setFont(font); painter.drawText(pixmap.rect(), Qt::AlignCenter, "❤️"); contributeButton->setIcon(QIcon(pixmap)); contributeButton->setToolTip(QString::fromUtf8(obs_module_text("VerticalDonate"))); contributeButton->setStyleSheet( QString::fromUtf8("QPushButton{ border-top-right-radius: 0; border-bottom-right-radius: 0;}")); QPushButton::connect(contributeButton, &QPushButton::clicked, [] { QDesktopServices::openUrl(QUrl("https://aitum.tv/contribute")); }); aitumButtonGroupLayout->addWidget(contributeButton); auto aitumButton = new QPushButton; aitumButton->setMinimumHeight(30); aitumButton->setIcon(QIcon(":/aitum/media/aitum.png")); aitumButton->setToolTip(QString::fromUtf8("https://aitum.tv")); aitumButton->setStyleSheet(QString::fromUtf8("QPushButton{border-top-left-radius: 0; border-bottom-left-radius: 0;}")); connect(aitumButton, &QPushButton::clicked, [] { QDesktopServices::openUrl(QUrl("https://aitum.tv")); }); aitumButtonGroupLayout->addWidget(aitumButton); buttonRow->addLayout(aitumButtonGroupLayout); 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(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(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(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 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(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, "Aitum 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(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 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(event)); case QEvent::MouseButtonRelease: return this->HandleMouseReleaseEvent(static_cast(event)); //case QEvent::MouseButtonDblClick: return this->HandleMouseClickEvent( static_cast(event)); case QEvent::MouseMove: return this->HandleMouseMoveEvent(static_cast(event)); //case QEvent::Enter: case QEvent::Leave: return this->HandleMouseLeaveEvent(static_cast(event)); case QEvent::Wheel: return this->HandleMouseWheelEvent(static_cast(event)); //case QEvent::FocusIn: //case QEvent::FocusOut: case QEvent::KeyPress: return this->HandleKeyPressEvent(static_cast(event)); case QEvent::KeyRelease: return this->HandleKeyReleaseEvent(static_cast(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 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 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(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(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(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 *>(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 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, ¢er); // 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(obs_frontend_get_main_window()); auto transformDialog = mainDialog->findChild("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("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 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 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 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 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(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(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(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 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(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(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(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(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 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(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(param); auto parent = qobject_cast(menu->parent()); auto a = parent && !parent->menuAction()->data().isNull() ? parent->menuAction() : menu->menuAction(); while (parent && qobject_cast(parent->parent())) { parent = qobject_cast(parent->parent()); } CanvasDock *cd = static_cast(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 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 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 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(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(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(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 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(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(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(data); d->CheckReplayBuffer(); } void CanvasDock::replay_saved(void *data, calldata_t *calldata) { UNUSED_PARAMETER(calldata); auto d = static_cast(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(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(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::iterator it) { CreateStreamOutput(it); const bool started_video = StartVideo(); if (it->settings && obs_data_get_bool(it->settings, "advanced") && obs_get_module("aitum-multistream")) { 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 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::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") && obs_get_module("aitum-multistream")) { 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(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(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 CanvasDock::GetScenes() { std::vector 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 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 *)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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(obs_frontend_get_main_window()); switch (icon_type) { case OBS_ICON_TYPE_IMAGE: return main_window->property("imageIcon").value(); case OBS_ICON_TYPE_COLOR: return main_window->property("colorIcon").value(); case OBS_ICON_TYPE_SLIDESHOW: return main_window->property("slideshowIcon").value(); case OBS_ICON_TYPE_AUDIO_INPUT: return main_window->property("audioInputIcon").value(); case OBS_ICON_TYPE_AUDIO_OUTPUT: return main_window->property("audioOutputIcon").value(); case OBS_ICON_TYPE_DESKTOP_CAPTURE: return main_window->property("desktopCapIcon").value(); case OBS_ICON_TYPE_WINDOW_CAPTURE: return main_window->property("windowCapIcon").value(); case OBS_ICON_TYPE_GAME_CAPTURE: return main_window->property("gameCapIcon").value(); case OBS_ICON_TYPE_CAMERA: return main_window->property("cameraIcon").value(); case OBS_ICON_TYPE_TEXT: return main_window->property("textIcon").value(); case OBS_ICON_TYPE_MEDIA: return main_window->property("mediaIcon").value(); case OBS_ICON_TYPE_BROWSER: return main_window->property("browserIcon").value(); case OBS_ICON_TYPE_CUSTOM: //TODO: Add ability for sources to define custom icons return main_window->property("defaultIcon").value(); case OBS_ICON_TYPE_PROCESS_AUDIO_OUTPUT: return main_window->property("audioProcessOutputIcon").value(); default: return main_window->property("defaultIcon").value(); } } QIcon CanvasDock::GetSceneIcon() const { const auto main_window = static_cast(obs_frontend_get_main_window()); return main_window->property("sceneIcon").value(); } QIcon CanvasDock::GetGroupIcon() const { const auto main_window = static_cast(obs_frontend_get_main_window()); return main_window->property("groupIcon").value(); } 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(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(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(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(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 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(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, "[Aitum 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(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) { QDesktopServices::openUrl(QUrl(QString::fromUtf8("https://aitum.tv/download/stream-suite"))); } 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()); } }